From 467f9d95c1eb79faa2d6baea73e8776f1b1f105e Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 7 Feb 2024 12:22:09 +0000 Subject: [PATCH 001/313] - --- build.sbt | 12 +++++++- .../org/terminal21/server/Dependencies.scala | 6 ++++ .../terminal21/server/Terminal21Server.scala | 0 .../serverapp/ServerSideSessions.scala | 29 +++++++++++++++++++ .../scala/org/terminal21/server/Routes.scala | 4 +-- .../{Dependencies.scala => ServerBeans.scala} | 2 +- .../service/ServerSessionsService.scala | 2 +- .../org/terminal21/client/Sessions.scala | 5 ++-- 8 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 terminal21-server-app/src/main/scala/org/terminal21/server/Dependencies.scala rename {terminal21-server => terminal21-server-app}/src/main/scala/org/terminal21/server/Terminal21Server.scala (100%) create mode 100644 terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideSessions.scala rename terminal21-server/src/main/scala/org/terminal21/server/{Dependencies.scala => ServerBeans.scala} (59%) diff --git a/build.sbt b/build.sbt index 495aa8ae..711fb57d 100644 --- a/build.sbt +++ b/build.sbt @@ -5,7 +5,7 @@ import sbt.librarymanagement.ModuleFilter */ val scala3Version = "3.3.1" -ThisBuild / version := "0.21" +ThisBuild / version := "0.30" ThisBuild / organization := "io.github.kostaskougios" name := "rest-api" ThisBuild / scalaVersion := scala3Version @@ -102,6 +102,14 @@ lazy val `terminal21-server` = project .dependsOn(`terminal21-ui-std-exports` % "compile->compile;test->test", `terminal21-server-client-common`) .enablePlugins(FunctionsRemotePlugin) +lazy val `terminal21-server-app` = project + .settings( + commonSettings, + libraryDependencies ++= Seq( + ) + ) + .dependsOn(`terminal21-server` % "compile->compile;test->test", `terminal21-ui-std`) + lazy val `terminal21-ui-std-exports` = project .settings( commonSettings, @@ -147,6 +155,7 @@ lazy val `terminal21-ui-std` = project lazy val `end-to-end-tests` = project .settings( commonSettings, + publish := {}, libraryDependencies ++= Seq(ScalaTest, LogBack) ) .dependsOn(`terminal21-ui-std`, `terminal21-nivo`, `terminal21-mathjax`) @@ -185,6 +194,7 @@ lazy val `terminal21-mathjax` = project lazy val `terminal21-code-generation`: Project = project .settings( commonSettings, + publish := {}, libraryDependencies ++= Seq( ScalaTest, Scala3Tasty, diff --git a/terminal21-server-app/src/main/scala/org/terminal21/server/Dependencies.scala b/terminal21-server-app/src/main/scala/org/terminal21/server/Dependencies.scala new file mode 100644 index 00000000..0b35ceec --- /dev/null +++ b/terminal21-server-app/src/main/scala/org/terminal21/server/Dependencies.scala @@ -0,0 +1,6 @@ +package org.terminal21.server + +import functions.fibers.FiberExecutor +import org.terminal21.serverapp.ServerSideSessionsBeans + +class Dependencies(val fiberExecutor: FiberExecutor) extends ServerBeans with ServerSideSessionsBeans diff --git a/terminal21-server/src/main/scala/org/terminal21/server/Terminal21Server.scala b/terminal21-server-app/src/main/scala/org/terminal21/server/Terminal21Server.scala similarity index 100% rename from terminal21-server/src/main/scala/org/terminal21/server/Terminal21Server.scala rename to terminal21-server-app/src/main/scala/org/terminal21/server/Terminal21Server.scala diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideSessions.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideSessions.scala new file mode 100644 index 00000000..7ff9b6c9 --- /dev/null +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideSessions.scala @@ -0,0 +1,29 @@ +package org.terminal21.serverapp + +import org.terminal21.client.ConnectedSession +import org.terminal21.client.components.{ComponentLib, StdElementEncoding, UiElementEncoding} +import org.terminal21.config.Config +import org.terminal21.ui.std.SessionsService + +import java.util.concurrent.atomic.AtomicBoolean + +class ServerSideSessions(sessionsService: SessionsService): + def withNewSession[R](id: String, name: String, componentLibs: ComponentLib*)(f: ConnectedSession => R): R = + val config = Config.Default + val serverUrl = s"http://${config.host}:${config.port}" + + val session = sessionsService.createSession(id, name) + val encoding = new UiElementEncoding(Seq(StdElementEncoding) ++ componentLibs) + val isStopped = new AtomicBoolean(false) + def terminate(): Unit = + isStopped.set(true) + + val connectedSession = ConnectedSession(session, encoding, serverUrl, sessionsService, terminate) + try + f(connectedSession) + finally + if !isStopped.get() && !connectedSession.isLeaveSessionOpen then sessionsService.terminateSession(session) + +trait ServerSideSessionsBeans: + def sessionsService: SessionsService + lazy val serverSideSessions = new ServerSideSessions(sessionsService) diff --git a/terminal21-server/src/main/scala/org/terminal21/server/Routes.scala b/terminal21-server/src/main/scala/org/terminal21/server/Routes.scala index f0168f76..544af7d6 100644 --- a/terminal21-server/src/main/scala/org/terminal21/server/Routes.scala +++ b/terminal21-server/src/main/scala/org/terminal21/server/Routes.scala @@ -12,7 +12,7 @@ import java.nio.file.Path object Routes: private val logger = LoggerFactory.getLogger(getClass) - def register(dependencies: Dependencies, rb: HttpRouting.Builder): Unit = + def register(dependencies: ServerBeans, rb: HttpRouting.Builder): Unit = import dependencies.* SessionsServiceReceiverFactory.newJsonSessionsServiceHelidonRoutes(sessionsService).routes(rb) @@ -34,7 +34,7 @@ object Routes: rb.register("/ui", staticContent) rb.register("/web", publicContent) - def ws(dependencies: Dependencies): WsRouting.Builder = + def ws(dependencies: ServerBeans): WsRouting.Builder = val b = WsRouting.builder b.endpoint("/ui/sessions", dependencies.sessionsWebSocket) .endpoint("/api/command-ws", dependencies.commandWebSocket.commandWebSocketListener.listener) diff --git a/terminal21-server/src/main/scala/org/terminal21/server/Dependencies.scala b/terminal21-server/src/main/scala/org/terminal21/server/ServerBeans.scala similarity index 59% rename from terminal21-server/src/main/scala/org/terminal21/server/Dependencies.scala rename to terminal21-server/src/main/scala/org/terminal21/server/ServerBeans.scala index c799e449..ddb34dda 100644 --- a/terminal21-server/src/main/scala/org/terminal21/server/Dependencies.scala +++ b/terminal21-server/src/main/scala/org/terminal21/server/ServerBeans.scala @@ -4,4 +4,4 @@ import functions.fibers.FiberExecutor import org.terminal21.server.service.{CommandWebSocketBeans, ServerSessionsServiceBeans} import org.terminal21.server.ui.SessionsWebSocketBeans -class Dependencies(val fiberExecutor: FiberExecutor) extends ServerSessionsServiceBeans with SessionsWebSocketBeans with CommandWebSocketBeans +trait ServerBeans extends ServerSessionsServiceBeans with SessionsWebSocketBeans with CommandWebSocketBeans diff --git a/terminal21-server/src/main/scala/org/terminal21/server/service/ServerSessionsService.scala b/terminal21-server/src/main/scala/org/terminal21/server/service/ServerSessionsService.scala index d8749662..6e4688aa 100644 --- a/terminal21-server/src/main/scala/org/terminal21/server/service/ServerSessionsService.scala +++ b/terminal21-server/src/main/scala/org/terminal21/server/service/ServerSessionsService.scala @@ -79,4 +79,4 @@ class ServerSessionsService extends SessionsService: state.eventsNotificationRegistry.add(listener) trait ServerSessionsServiceBeans: - val sessionsService: ServerSessionsService = new ServerSessionsService + lazy val sessionsService: ServerSessionsService = new ServerSessionsService diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Sessions.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Sessions.scala index 5b29b5fd..41776398 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Sessions.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Sessions.scala @@ -34,8 +34,7 @@ object Sessions: val listener = new ClientEventsWsListener(wsClient, connectedSession, executor) listener.start() - try { - f(connectedSession) - } finally + try f(connectedSession) + finally if !isStopped.get() && !connectedSession.isLeaveSessionOpen then sessionsService.terminateSession(session) listener.close() From 0841056db92b7d0226e63fbfd1af33892233369b Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 7 Feb 2024 12:46:08 +0000 Subject: [PATCH 002/313] - --- .../org/terminal21/server/Dependencies.scala | 3 +- .../terminal21/server/Terminal21Server.scala | 1 + .../terminal21/serverapp/ServerSideApp.scala | 4 +++ .../serverapp/bundled/AppManager.scala | 29 +++++++++++++++++++ 4 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideApp.scala create mode 100644 terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala diff --git a/terminal21-server-app/src/main/scala/org/terminal21/server/Dependencies.scala b/terminal21-server-app/src/main/scala/org/terminal21/server/Dependencies.scala index 0b35ceec..47501131 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/server/Dependencies.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/server/Dependencies.scala @@ -2,5 +2,6 @@ package org.terminal21.server import functions.fibers.FiberExecutor import org.terminal21.serverapp.ServerSideSessionsBeans +import org.terminal21.serverapp.bundled.AppManagerBeans -class Dependencies(val fiberExecutor: FiberExecutor) extends ServerBeans with ServerSideSessionsBeans +class Dependencies(val fiberExecutor: FiberExecutor) extends ServerBeans with ServerSideSessionsBeans with AppManagerBeans diff --git a/terminal21-server-app/src/main/scala/org/terminal21/server/Terminal21Server.scala b/terminal21-server-app/src/main/scala/org/terminal21/server/Terminal21Server.scala index 70e8be52..be3383ad 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/server/Terminal21Server.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/server/Terminal21Server.scala @@ -27,6 +27,7 @@ object Terminal21Server: .build .start + dependencies.appManager.start() if !server.isRunning then throw new IllegalStateException("Server failed to start") try logger.info(s"Terminal 21 Server started. Please open http://localhost:$portV/ui for the user interface") diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideApp.scala new file mode 100644 index 00000000..ccc6a5bd --- /dev/null +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideApp.scala @@ -0,0 +1,4 @@ +package org.terminal21.serverapp + +trait ServerSideApp: + def createSession(serverSideSessions: ServerSideSessions): Unit diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala new file mode 100644 index 00000000..1f45e024 --- /dev/null +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala @@ -0,0 +1,29 @@ +package org.terminal21.serverapp.bundled + +import functions.fibers.FiberExecutor +import org.slf4j.LoggerFactory +import org.terminal21.client.ConnectedSession +import org.terminal21.client.components.* +import org.terminal21.client.components.std.Paragraph +import org.terminal21.server.Terminal21Server.getClass +import org.terminal21.serverapp.ServerSideSessions + +class AppManager(serverSideSessions: ServerSideSessions, fiberExecutor: FiberExecutor): + private val logger = LoggerFactory.getLogger(getClass) + + def start(): Unit = + fiberExecutor.submit: + logger.info("Starting AppManager") + serverSideSessions.withNewSession("app-manager", "Apps"): session => + given ConnectedSession = session + + Seq( + Paragraph(text = "Here you can run the apps installed on the server") + ).render() + + session.waitTillUserClosesSession() + +trait AppManagerBeans: + def serverSideSessions: ServerSideSessions + def fiberExecutor: FiberExecutor + lazy val appManager = new AppManager(serverSideSessions, fiberExecutor) From 971e98063bc615d79af9307cf1cda2e02181a184 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 7 Feb 2024 13:59:57 +0000 Subject: [PATCH 003/313] - --- .../org/terminal21/server/Dependencies.scala | 4 ++-- .../terminal21/server/Terminal21Server.scala | 8 +++++--- .../terminal21/serverapp/ServerSideApp.scala | 1 + .../serverapp/bundled/AppManager.scala | 19 ++++++++++++++----- .../serverapp/bundled/DefaultApps.scala | 6 ++++++ .../serverapp/bundled/ServerStatusApp.scala | 10 ++++++++++ 6 files changed, 38 insertions(+), 10 deletions(-) create mode 100644 terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/DefaultApps.scala create mode 100644 terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala diff --git a/terminal21-server-app/src/main/scala/org/terminal21/server/Dependencies.scala b/terminal21-server-app/src/main/scala/org/terminal21/server/Dependencies.scala index 47501131..aa3f1758 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/server/Dependencies.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/server/Dependencies.scala @@ -1,7 +1,7 @@ package org.terminal21.server import functions.fibers.FiberExecutor -import org.terminal21.serverapp.ServerSideSessionsBeans +import org.terminal21.serverapp.{ServerSideApp, ServerSideSessionsBeans} import org.terminal21.serverapp.bundled.AppManagerBeans -class Dependencies(val fiberExecutor: FiberExecutor) extends ServerBeans with ServerSideSessionsBeans with AppManagerBeans +class Dependencies(val fiberExecutor: FiberExecutor, val apps: Seq[ServerSideApp]) extends ServerBeans with ServerSideSessionsBeans with AppManagerBeans diff --git a/terminal21-server-app/src/main/scala/org/terminal21/server/Terminal21Server.scala b/terminal21-server-app/src/main/scala/org/terminal21/server/Terminal21Server.scala index be3383ad..0d0e42a3 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/server/Terminal21Server.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/server/Terminal21Server.scala @@ -6,15 +6,17 @@ import io.helidon.webserver.WebServer import io.helidon.webserver.http.HttpRouting import org.slf4j.LoggerFactory import org.terminal21.config.Config +import org.terminal21.serverapp.ServerSideApp +import org.terminal21.serverapp.bundled.DefaultApps import java.net.InetAddress object Terminal21Server: - private val logger = LoggerFactory.getLogger(getClass) - def start(port: Option[Int] = None): Unit = + private val logger = LoggerFactory.getLogger(getClass) + def start(port: Option[Int] = None, apps: Seq[ServerSideApp] = Nil, defaultApps: Seq[ServerSideApp] = DefaultApps.All): Unit = FiberExecutor.withFiberExecutor: executor => val portV = port.getOrElse(Config.Default.port) - val dependencies = new Dependencies(executor) + val dependencies = new Dependencies(executor, apps ++ defaultApps) val routesBuilder = HttpRouting.builder() Routes.register(dependencies, routesBuilder) Routes.static(routesBuilder) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideApp.scala index ccc6a5bd..44c08a67 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideApp.scala @@ -1,4 +1,5 @@ package org.terminal21.serverapp trait ServerSideApp: + def name: String def createSession(serverSideSessions: ServerSideSessions): Unit diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala index 1f45e024..c6078e7c 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala @@ -4,11 +4,12 @@ import functions.fibers.FiberExecutor import org.slf4j.LoggerFactory import org.terminal21.client.ConnectedSession import org.terminal21.client.components.* +import org.terminal21.client.components.chakra.* import org.terminal21.client.components.std.Paragraph import org.terminal21.server.Terminal21Server.getClass -import org.terminal21.serverapp.ServerSideSessions +import org.terminal21.serverapp.{ServerSideApp, ServerSideSessions} -class AppManager(serverSideSessions: ServerSideSessions, fiberExecutor: FiberExecutor): +class AppManager(serverSideSessions: ServerSideSessions, fiberExecutor: FiberExecutor, apps: Seq[ServerSideApp]): private val logger = LoggerFactory.getLogger(getClass) def start(): Unit = @@ -17,13 +18,21 @@ class AppManager(serverSideSessions: ServerSideSessions, fiberExecutor: FiberExe serverSideSessions.withNewSession("app-manager", "Apps"): session => given ConnectedSession = session - Seq( + val appLinks = apps.map: app => + Link(text = app.name).onClick: () => + startApp(app) + + (Seq( Paragraph(text = "Here you can run the apps installed on the server") - ).render() + ) ++ appLinks).render() session.waitTillUserClosesSession() + private def startApp(app: ServerSideApp): Unit = + app.createSession(serverSideSessions) + trait AppManagerBeans: def serverSideSessions: ServerSideSessions def fiberExecutor: FiberExecutor - lazy val appManager = new AppManager(serverSideSessions, fiberExecutor) + def apps: Seq[ServerSideApp] + lazy val appManager = new AppManager(serverSideSessions, fiberExecutor, apps) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/DefaultApps.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/DefaultApps.scala new file mode 100644 index 00000000..e9bcc63a --- /dev/null +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/DefaultApps.scala @@ -0,0 +1,6 @@ +package org.terminal21.serverapp.bundled + +object DefaultApps: + val All = Seq( + new ServerStatusApp + ) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala new file mode 100644 index 00000000..5e602951 --- /dev/null +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -0,0 +1,10 @@ +package org.terminal21.serverapp.bundled + +import org.terminal21.serverapp.{ServerSideApp, ServerSideSessions} + +class ServerStatusApp extends ServerSideApp: + override def name: String = "Server Status" + + override def createSession(serverSideSessions: ServerSideSessions): Unit = + println(s"$name creating session") + ??? From aa71fc599d4f9852387a496249df31c38b49b25e Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 7 Feb 2024 14:35:11 +0000 Subject: [PATCH 004/313] - --- .../org/terminal21/server/Dependencies.scala | 3 +- .../terminal21/serverapp/ServerSideApp.scala | 5 ++- .../serverapp/ServerSideSessions.scala | 14 +++++-- .../serverapp/bundled/AppManager.scala | 24 ++++++----- .../serverapp/bundled/ServerStatusApp.scala | 24 +++++++++-- .../terminal21/client/ConnectedSession.scala | 40 ++++++++++--------- 6 files changed, 73 insertions(+), 37 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/server/Dependencies.scala b/terminal21-server-app/src/main/scala/org/terminal21/server/Dependencies.scala index aa3f1758..f550ca07 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/server/Dependencies.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/server/Dependencies.scala @@ -4,4 +4,5 @@ import functions.fibers.FiberExecutor import org.terminal21.serverapp.{ServerSideApp, ServerSideSessionsBeans} import org.terminal21.serverapp.bundled.AppManagerBeans -class Dependencies(val fiberExecutor: FiberExecutor, val apps: Seq[ServerSideApp]) extends ServerBeans with ServerSideSessionsBeans with AppManagerBeans +class Dependencies(val fiberExecutor: FiberExecutor, val apps: Seq[ServerSideApp]) extends ServerBeans with ServerSideSessionsBeans with AppManagerBeans: + override def dependencies: Dependencies = this diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideApp.scala index 44c08a67..e057414c 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideApp.scala @@ -1,5 +1,8 @@ package org.terminal21.serverapp +import org.terminal21.server.Dependencies + trait ServerSideApp: def name: String - def createSession(serverSideSessions: ServerSideSessions): Unit + def description: String + def createSession(serverSideSessions: ServerSideSessions, dependencies: Dependencies): Unit diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideSessions.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideSessions.scala index 7ff9b6c9..fdea4b17 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideSessions.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideSessions.scala @@ -1,13 +1,14 @@ package org.terminal21.serverapp +import functions.fibers.FiberExecutor import org.terminal21.client.ConnectedSession import org.terminal21.client.components.{ComponentLib, StdElementEncoding, UiElementEncoding} import org.terminal21.config.Config -import org.terminal21.ui.std.SessionsService +import org.terminal21.server.service.ServerSessionsService import java.util.concurrent.atomic.AtomicBoolean -class ServerSideSessions(sessionsService: SessionsService): +class ServerSideSessions(sessionsService: ServerSessionsService, executor: FiberExecutor): def withNewSession[R](id: String, name: String, componentLibs: ComponentLib*)(f: ConnectedSession => R): R = val config = Config.Default val serverUrl = s"http://${config.host}:${config.port}" @@ -19,11 +20,16 @@ class ServerSideSessions(sessionsService: SessionsService): isStopped.set(true) val connectedSession = ConnectedSession(session, encoding, serverUrl, sessionsService, terminate) + sessionsService.notifyMeOnSessionEvents(session): event => + executor.submit: + connectedSession.fireEvent(event) + true try f(connectedSession) finally if !isStopped.get() && !connectedSession.isLeaveSessionOpen then sessionsService.terminateSession(session) trait ServerSideSessionsBeans: - def sessionsService: SessionsService - lazy val serverSideSessions = new ServerSideSessions(sessionsService) + def sessionsService: ServerSessionsService + def fiberExecutor: FiberExecutor + lazy val serverSideSessions = new ServerSideSessions(sessionsService, fiberExecutor) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala index c6078e7c..6b09d623 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala @@ -5,11 +5,11 @@ import org.slf4j.LoggerFactory import org.terminal21.client.ConnectedSession import org.terminal21.client.components.* import org.terminal21.client.components.chakra.* -import org.terminal21.client.components.std.Paragraph +import org.terminal21.server.Dependencies import org.terminal21.server.Terminal21Server.getClass import org.terminal21.serverapp.{ServerSideApp, ServerSideSessions} -class AppManager(serverSideSessions: ServerSideSessions, fiberExecutor: FiberExecutor, apps: Seq[ServerSideApp]): +class AppManager(serverSideSessions: ServerSideSessions, fiberExecutor: FiberExecutor, apps: Seq[ServerSideApp], dependencies: Dependencies): private val logger = LoggerFactory.getLogger(getClass) def start(): Unit = @@ -18,21 +18,27 @@ class AppManager(serverSideSessions: ServerSideSessions, fiberExecutor: FiberExe serverSideSessions.withNewSession("app-manager", "Apps"): session => given ConnectedSession = session - val appLinks = apps.map: app => - Link(text = app.name).onClick: () => + val appRows = apps.map: app => + val link = Link(text = app.name).onClick: () => startApp(app) + Seq[UiElement](link, Text(text = app.description)) + val appsTable = QuickTable( + caption = Some("Apps installed on the server"), + rows = Seq(Seq(Text(text = "App Name"), Text(text = "Description"))) ++ appRows + ) - (Seq( - Paragraph(text = "Here you can run the apps installed on the server") - ) ++ appLinks).render() + Seq( + appsTable + ).render() session.waitTillUserClosesSession() private def startApp(app: ServerSideApp): Unit = - app.createSession(serverSideSessions) + app.createSession(serverSideSessions, dependencies) trait AppManagerBeans: def serverSideSessions: ServerSideSessions def fiberExecutor: FiberExecutor def apps: Seq[ServerSideApp] - lazy val appManager = new AppManager(serverSideSessions, fiberExecutor, apps) + def dependencies: Dependencies + lazy val appManager = new AppManager(serverSideSessions, fiberExecutor, apps, dependencies) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 5e602951..554999b3 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -1,10 +1,26 @@ package org.terminal21.serverapp.bundled +import org.terminal21.client.ConnectedSession +import org.terminal21.client.components.* +import org.terminal21.client.components.chakra.* +import org.terminal21.server.Dependencies import org.terminal21.serverapp.{ServerSideApp, ServerSideSessions} class ServerStatusApp extends ServerSideApp: - override def name: String = "Server Status" + override def name: String = "Server Status" + override def description: String = "Status of the server." - override def createSession(serverSideSessions: ServerSideSessions): Unit = - println(s"$name creating session") - ??? + override def createSession(serverSideSessions: ServerSideSessions, dependencies: Dependencies): Unit = + serverSideSessions.withNewSession("server-status", "Server Status"): session => + given ConnectedSession = session + val sessionService = dependencies.sessionsService + val sessions = sessionService.allSessions + val sessionsTable = QuickTable(caption = Some("All sessions")) + .headers("Id", "Name", "Is Open") + .rows( + sessions.map: session => + Seq(session.id, session.name, session.isOpen) + ) + + Seq(sessionsTable).render() + session.leaveSessionOpenAfterExiting() diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index e693d5c5..a6d81dee 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -58,24 +58,28 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se def click(e: UiElement): Unit = fireEvent(OnClick(e.key)) - private[client] def fireEvent(event: CommandEvent): Unit = - event match - case SessionClosed(_) => - exitLatch.countDown() - onCloseHandler() - case _ => - handlers.getEventHandler(event.key) match - case Some(handlers) => - for handler <- handlers do - (event, handler) match - case (_: OnClick, h: OnClickEventHandler) => h.onClick() - case (onChange: OnChange, h: OnChangeEventHandler) => h.onChange(onChange.value) - case (onChange: OnChange, h: OnChangeBooleanEventHandler) => h.onChange(onChange.value.toBoolean) - case x => logger.error(s"Unknown event handling combination : $x") - case None => - logger.warn(s"There is no event handler for event $event") - - def render(es: UiElement*): Unit = + def fireEvent(event: CommandEvent): Unit = + try + event match + case SessionClosed(_) => + exitLatch.countDown() + onCloseHandler() + case _ => + handlers.getEventHandler(event.key) match + case Some(handlers) => + for handler <- handlers do + (event, handler) match + case (_: OnClick, h: OnClickEventHandler) => h.onClick() + case (onChange: OnChange, h: OnChangeEventHandler) => h.onChange(onChange.value) + case (onChange: OnChange, h: OnChangeBooleanEventHandler) => h.onChange(onChange.value.toBoolean) + case x => logger.error(s"Unknown event handling combination : $x") + case None => + logger.warn(s"There is no event handler for event $event") + catch + case t: Throwable => + logger.error(s"Session ${session.id}: An error occurred while handling $event", t) + throw t + def render(es: UiElement*): Unit = handlers.registerEventHandlers(es) val j = toJson(es) sessionsService.setSessionJsonState(session, j) From 830d37e49967123ca48ed11395e027061801be74 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 7 Feb 2024 14:49:14 +0000 Subject: [PATCH 005/313] - --- .../serverapp/bundled/AppManager.scala | 4 +-- .../serverapp/bundled/ServerStatusApp.scala | 25 +++++++++++++------ .../components/chakra/ChakraElement.scala | 2 ++ 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala index 6b09d623..7e9aeba1 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala @@ -24,8 +24,8 @@ class AppManager(serverSideSessions: ServerSideSessions, fiberExecutor: FiberExe Seq[UiElement](link, Text(text = app.description)) val appsTable = QuickTable( caption = Some("Apps installed on the server"), - rows = Seq(Seq(Text(text = "App Name"), Text(text = "Description"))) ++ appRows - ) + rows = appRows + ).headers("App Name", "Description") Seq( appsTable diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 554999b3..581639d4 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -3,8 +3,11 @@ package org.terminal21.serverapp.bundled import org.terminal21.client.ConnectedSession import org.terminal21.client.components.* import org.terminal21.client.components.chakra.* +import org.terminal21.model.Session import org.terminal21.server.Dependencies +import org.terminal21.server.service.ServerSessionsService import org.terminal21.serverapp.{ServerSideApp, ServerSideSessions} +import org.terminal21.ui.std.SessionsService class ServerStatusApp extends ServerSideApp: override def name: String = "Server Status" @@ -15,12 +18,20 @@ class ServerStatusApp extends ServerSideApp: given ConnectedSession = session val sessionService = dependencies.sessionsService val sessions = sessionService.allSessions - val sessionsTable = QuickTable(caption = Some("All sessions")) - .headers("Id", "Name", "Is Open") - .rows( - sessions.map: session => - Seq(session.id, session.name, session.isOpen) - ) + val sessionsTable = QuickTable( + caption = Some("All sessions"), + rows = sessions.map: session => + Seq(Text(text = session.id), Text(text = session.name), if session.isOpen then CheckIcon() else NotAllowedIcon(), actionsFor(session, sessionService)) + ) + .headers("Id", "Name", "Is Open", "Actions") Seq(sessionsTable).render() - session.leaveSessionOpenAfterExiting() + session.waitTillUserClosesSession() + + def actionsFor(session: Session, sessionsService: ServerSessionsService)(using ConnectedSession): UiElement = + if session.isOpen then + Button(text = "Close") + .withLeftIcon(CloseIcon()) + .onClick: () => + sessionsService.terminateSession(session) + else NotAllowedIcon() diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala index ef601436..0c5ae15a 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala @@ -37,7 +37,9 @@ case class Button( def withVariant(v: Option[String]) = copy(variant = v) def withColorScheme(v: Option[String]) = copy(colorScheme = v) def withLeftIcon(v: Option[UiElement]) = copy(leftIcon = v) + def withLeftIcon(v: UiElement) = copy(leftIcon = Some(v)) def withRightIcon(v: Option[UiElement]) = copy(rightIcon = v) + def withRightIcon(v: UiElement) = copy(rightIcon = Some(v)) def withIsActive(v: Option[Boolean]) = copy(isActive = v) def withIsDisabled(v: Option[Boolean]) = copy(isDisabled = v) def withIsLoading(v: Option[Boolean]) = copy(isLoading = v) From b4045cc03655308cf6ece82d7653ef72d21443c4 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 7 Feb 2024 15:13:57 +0000 Subject: [PATCH 006/313] - --- .../org/terminal21/serverapp/bundled/ServerStatusApp.scala | 7 +++---- .../src/main/scala/org/terminal21/model/Session.scala | 2 +- .../terminal21/server/service/ServerSessionsService.scala | 4 ++++ 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 581639d4..034a25fa 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -7,7 +7,6 @@ import org.terminal21.model.Session import org.terminal21.server.Dependencies import org.terminal21.server.service.ServerSessionsService import org.terminal21.serverapp.{ServerSideApp, ServerSideSessions} -import org.terminal21.ui.std.SessionsService class ServerStatusApp extends ServerSideApp: override def name: String = "Server Status" @@ -28,10 +27,10 @@ class ServerStatusApp extends ServerSideApp: Seq(sessionsTable).render() session.waitTillUserClosesSession() - def actionsFor(session: Session, sessionsService: ServerSessionsService)(using ConnectedSession): UiElement = + private def actionsFor(session: Session, sessionsService: ServerSessionsService)(using ConnectedSession): UiElement = if session.isOpen then - Button(text = "Close") + Button(text = "Close", size = Some("sm")) .withLeftIcon(CloseIcon()) .onClick: () => - sessionsService.terminateSession(session) + sessionsService.terminateAndRemove(session) else NotAllowedIcon() diff --git a/terminal21-server-client-common/src/main/scala/org/terminal21/model/Session.scala b/terminal21-server-client-common/src/main/scala/org/terminal21/model/Session.scala index 1165974e..05db6f42 100644 --- a/terminal21-server-client-common/src/main/scala/org/terminal21/model/Session.scala +++ b/terminal21-server-client-common/src/main/scala/org/terminal21/model/Session.scala @@ -2,4 +2,4 @@ package org.terminal21.model case class Session(id: String, name: String, secret: String, isOpen: Boolean): def hideSecret: Session = copy(secret = "***") - def close: Session = copy(isOpen=false) + def close: Session = copy(isOpen = false) diff --git a/terminal21-server/src/main/scala/org/terminal21/server/service/ServerSessionsService.scala b/terminal21-server/src/main/scala/org/terminal21/server/service/ServerSessionsService.scala index 6e4688aa..434e3510 100644 --- a/terminal21-server/src/main/scala/org/terminal21/server/service/ServerSessionsService.scala +++ b/terminal21-server/src/main/scala/org/terminal21/server/service/ServerSessionsService.scala @@ -36,6 +36,10 @@ class ServerSessionsService extends SessionsService: sessions += session.close -> state.close sessionChangeNotificationRegistry.notifyAll(allSessions) + def terminateAndRemove(session: Session): Unit = + terminateSession(session) + removeSession(session.close) + override def createSession(id: String, name: String): Session = val s = Session(id, name, UUID.randomUUID().toString, true) logger.info(s"Creating session $s") From 295840418638c96bc89dfec8c422887834916094 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 7 Feb 2024 15:41:11 +0000 Subject: [PATCH 007/313] - --- .../serverapp/bundled/ServerStatusApp.scala | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 034a25fa..e3cd63bb 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -15,22 +15,29 @@ class ServerStatusApp extends ServerSideApp: override def createSession(serverSideSessions: ServerSideSessions, dependencies: Dependencies): Unit = serverSideSessions.withNewSession("server-status", "Server Status"): session => given ConnectedSession = session - val sessionService = dependencies.sessionsService - val sessions = sessionService.allSessions - val sessionsTable = QuickTable( - caption = Some("All sessions"), - rows = sessions.map: session => - Seq(Text(text = session.id), Text(text = session.name), if session.isOpen then CheckIcon() else NotAllowedIcon(), actionsFor(session, sessionService)) - ) - .headers("Id", "Name", "Is Open", "Actions") + new ServerStatusAppInternal(dependencies.sessionsService).run() - Seq(sessionsTable).render() - session.waitTillUserClosesSession() +private class ServerStatusAppInternal(sessionsService: ServerSessionsService)(using session: ConnectedSession): + def run(): Unit = + updateStatus() + session.waitTillUserClosesSession() - private def actionsFor(session: Session, sessionsService: ServerSessionsService)(using ConnectedSession): UiElement = + private def updateStatus() = + val sessions = sessionsService.allSessions + val sessionsTable = QuickTable( + caption = Some("All sessions"), + rows = sessions.map: session => + Seq(Text(text = session.id), Text(text = session.name), if session.isOpen then CheckIcon() else NotAllowedIcon(), actionsFor(session)) + ) + .headers("Id", "Name", "Is Open", "Actions") + + Seq(sessionsTable).render() + + private def actionsFor(session: Session)(using ConnectedSession): UiElement = if session.isOpen then Button(text = "Close", size = Some("sm")) .withLeftIcon(CloseIcon()) .onClick: () => sessionsService.terminateAndRemove(session) + updateStatus() else NotAllowedIcon() From cbc7d85e4e5d4a7233d932c6f814ce61b5a5fd4f Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 8 Feb 2024 12:13:29 +0000 Subject: [PATCH 008/313] - --- build.sbt | 6 ++- .../serverapp/bundled/ServerStatusApp.scala | 2 +- .../serverapp/ServerSideSessionsTest.scala | 37 +++++++++++++++++++ 3 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 terminal21-server-app/src/test/scala/org/terminal21/serverapp/ServerSideSessionsTest.scala diff --git a/build.sbt b/build.sbt index 711fb57d..52abdab2 100644 --- a/build.sbt +++ b/build.sbt @@ -27,8 +27,9 @@ val FunctionsHelidonClient = "io.github.kostaskougios" %% "helidon-client" val FunctionsHelidonWsClient = "io.github.kostaskougios" %% "helidon-ws-client" % FunctionsVersion val FunctionsFibers = "io.github.kostaskougios" %% "fibers" % FunctionsVersion -val ScalaTest = "org.scalatest" %% "scalatest" % "3.2.15" % Test +val ScalaTest = "org.scalatest" %% "scalatest" % "3.2.18" % Test val Mockito = "org.mockito" % "mockito-all" % "2.0.2-beta" % Test +val Mockito510 = "org.scalatestplus" %% "mockito-5-10" % "3.2.18.0" % Test val Scala3Tasty = "org.scala-lang" %% "scala3-tasty-inspector" % scala3Version val CommonsText = "org.apache.commons" % "commons-text" % "1.10.0" val CommonsIO = "commons-io" % "commons-io" % "2.11.0" @@ -106,9 +107,10 @@ lazy val `terminal21-server-app` = project .settings( commonSettings, libraryDependencies ++= Seq( + Mockito510 ) ) - .dependsOn(`terminal21-server` % "compile->compile;test->test", `terminal21-ui-std`) + .dependsOn(`terminal21-server` % "compile->compile;test->test", `terminal21-ui-std`, `terminal21-server-client-common` % "compile->compile;test->test") lazy val `terminal21-ui-std-exports` = project .settings( diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index e3cd63bb..1cf1d6cc 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -22,7 +22,7 @@ private class ServerStatusAppInternal(sessionsService: ServerSessionsService)(us updateStatus() session.waitTillUserClosesSession() - private def updateStatus() = + private def updateStatus(): Unit = val sessions = sessionsService.allSessions val sessionsTable = QuickTable( caption = Some("All sessions"), diff --git a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/ServerSideSessionsTest.scala b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/ServerSideSessionsTest.scala new file mode 100644 index 00000000..1bfc1f36 --- /dev/null +++ b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/ServerSideSessionsTest.scala @@ -0,0 +1,37 @@ +package org.terminal21.serverapp + +import functions.fibers.FiberExecutor +import org.mockito.Mockito +import org.mockito.Mockito.{verify, when} +import org.scalatest.BeforeAndAfterAll +import org.scalatest.funsuite.AnyFunSuiteLike +import org.scalatestplus.mockito.MockitoSugar.* +import org.terminal21.model.CommonModelBuilders +import org.terminal21.model.CommonModelBuilders.session +import org.terminal21.server.service.ServerSessionsService + +class ServerSideSessionsTest extends AnyFunSuiteLike with BeforeAndAfterAll: + val executor = FiberExecutor() + override protected def afterAll(): Unit = executor.shutdown() + + test("creates session"): + new App: + val s = session() + when(sessionsService.createSession(s.id, s.name)).thenReturn(s) + serverSideSessions.withNewSession(s.id, s.name): session => + session.leaveSessionOpenAfterExiting() + + verify(sessionsService).createSession(s.id, s.name) + + test("terminates session before exiting"): + new App: + val s = session() + when(sessionsService.createSession(s.id, s.name)).thenReturn(s) + serverSideSessions.withNewSession(s.id, s.name): _ => + () + + verify(sessionsService).terminateSession(s) + + class App: + val sessionsService = mock[ServerSessionsService] + val serverSideSessions = new ServerSideSessions(sessionsService, executor) From 391873e80a3ac8e028690d064eaf1efadca44fb1 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 8 Feb 2024 12:28:23 +0000 Subject: [PATCH 009/313] - --- .../serverapp/ServerSideSessions.scala | 44 +++++++++++-------- .../serverapp/bundled/AppManager.scala | 36 ++++++++------- .../serverapp/bundled/ServerStatusApp.scala | 8 ++-- .../serverapp/ServerSideSessionsTest.scala | 23 ++++++++-- 4 files changed, 68 insertions(+), 43 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideSessions.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideSessions.scala index fdea4b17..7b5e6ef8 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideSessions.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideSessions.scala @@ -9,25 +9,31 @@ import org.terminal21.server.service.ServerSessionsService import java.util.concurrent.atomic.AtomicBoolean class ServerSideSessions(sessionsService: ServerSessionsService, executor: FiberExecutor): - def withNewSession[R](id: String, name: String, componentLibs: ComponentLib*)(f: ConnectedSession => R): R = - val config = Config.Default - val serverUrl = s"http://${config.host}:${config.port}" - - val session = sessionsService.createSession(id, name) - val encoding = new UiElementEncoding(Seq(StdElementEncoding) ++ componentLibs) - val isStopped = new AtomicBoolean(false) - def terminate(): Unit = - isStopped.set(true) - - val connectedSession = ConnectedSession(session, encoding, serverUrl, sessionsService, terminate) - sessionsService.notifyMeOnSessionEvents(session): event => - executor.submit: - connectedSession.fireEvent(event) - true - try - f(connectedSession) - finally - if !isStopped.get() && !connectedSession.isLeaveSessionOpen then sessionsService.terminateSession(session) + case class Builder(id: String, name: String, componentLibs: Seq[ComponentLib] = Seq(StdElementEncoding)): + def andLibraries(libraries: ComponentLib*): Builder = copy(componentLibs = componentLibs ++ libraries) + + def connect[R](f: ConnectedSession => R): R = + val config = Config.Default + val serverUrl = s"http://${config.host}:${config.port}" + + val session = sessionsService.createSession(id, name) + val encoding = new UiElementEncoding(Seq(StdElementEncoding) ++ componentLibs) + val isStopped = new AtomicBoolean(false) + + def terminate(): Unit = + isStopped.set(true) + + val connectedSession = ConnectedSession(session, encoding, serverUrl, sessionsService, terminate) + sessionsService.notifyMeOnSessionEvents(session): event => + executor.submit: + connectedSession.fireEvent(event) + true + try + f(connectedSession) + finally + if !isStopped.get() && !connectedSession.isLeaveSessionOpen then sessionsService.terminateSession(session) + + def withNewSession(id: String, name: String): Builder = Builder(id, name) trait ServerSideSessionsBeans: def sessionsService: ServerSessionsService diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala index 7e9aeba1..dd94a979 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala @@ -15,23 +15,25 @@ class AppManager(serverSideSessions: ServerSideSessions, fiberExecutor: FiberExe def start(): Unit = fiberExecutor.submit: logger.info("Starting AppManager") - serverSideSessions.withNewSession("app-manager", "Apps"): session => - given ConnectedSession = session - - val appRows = apps.map: app => - val link = Link(text = app.name).onClick: () => - startApp(app) - Seq[UiElement](link, Text(text = app.description)) - val appsTable = QuickTable( - caption = Some("Apps installed on the server"), - rows = appRows - ).headers("App Name", "Description") - - Seq( - appsTable - ).render() - - session.waitTillUserClosesSession() + serverSideSessions + .withNewSession("app-manager", "Apps") + .connect: session => + given ConnectedSession = session + + val appRows = apps.map: app => + val link = Link(text = app.name).onClick: () => + startApp(app) + Seq[UiElement](link, Text(text = app.description)) + val appsTable = QuickTable( + caption = Some("Apps installed on the server"), + rows = appRows + ).headers("App Name", "Description") + + Seq( + appsTable + ).render() + + session.waitTillUserClosesSession() private def startApp(app: ServerSideApp): Unit = app.createSession(serverSideSessions, dependencies) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 1cf1d6cc..173a2f03 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -13,9 +13,11 @@ class ServerStatusApp extends ServerSideApp: override def description: String = "Status of the server." override def createSession(serverSideSessions: ServerSideSessions, dependencies: Dependencies): Unit = - serverSideSessions.withNewSession("server-status", "Server Status"): session => - given ConnectedSession = session - new ServerStatusAppInternal(dependencies.sessionsService).run() + serverSideSessions + .withNewSession("server-status", "Server Status") + .connect: session => + given ConnectedSession = session + new ServerStatusAppInternal(dependencies.sessionsService).run() private class ServerStatusAppInternal(sessionsService: ServerSessionsService)(using session: ConnectedSession): def run(): Unit = diff --git a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/ServerSideSessionsTest.scala b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/ServerSideSessionsTest.scala index 1bfc1f36..bbaa603e 100644 --- a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/ServerSideSessionsTest.scala +++ b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/ServerSideSessionsTest.scala @@ -18,8 +18,10 @@ class ServerSideSessionsTest extends AnyFunSuiteLike with BeforeAndAfterAll: new App: val s = session() when(sessionsService.createSession(s.id, s.name)).thenReturn(s) - serverSideSessions.withNewSession(s.id, s.name): session => - session.leaveSessionOpenAfterExiting() + serverSideSessions + .withNewSession(s.id, s.name) + .connect: session => + session.leaveSessionOpenAfterExiting() verify(sessionsService).createSession(s.id, s.name) @@ -27,11 +29,24 @@ class ServerSideSessionsTest extends AnyFunSuiteLike with BeforeAndAfterAll: new App: val s = session() when(sessionsService.createSession(s.id, s.name)).thenReturn(s) - serverSideSessions.withNewSession(s.id, s.name): _ => - () + serverSideSessions + .withNewSession(s.id, s.name) + .connect: _ => + () verify(sessionsService).terminateSession(s) + test("registers to receive events"): + new App: + val s = session() + when(sessionsService.createSession(s.id, s.name)).thenReturn(s) + serverSideSessions + .withNewSession(s.id, s.name) + .connect: _ => + () + + verify(sessionsService).notifyMeOnSessionEvents(s) + class App: val sessionsService = mock[ServerSessionsService] val serverSideSessions = new ServerSideSessions(sessionsService, executor) From b7e6dd4998a27e0d9139de2a02558daaeaa25b63 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 8 Feb 2024 12:29:55 +0000 Subject: [PATCH 010/313] - --- .../scala/org/terminal21/serverapp/ServerSideSessions.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideSessions.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideSessions.scala index 7b5e6ef8..93dd9dcf 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideSessions.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideSessions.scala @@ -9,8 +9,8 @@ import org.terminal21.server.service.ServerSessionsService import java.util.concurrent.atomic.AtomicBoolean class ServerSideSessions(sessionsService: ServerSessionsService, executor: FiberExecutor): - case class Builder(id: String, name: String, componentLibs: Seq[ComponentLib] = Seq(StdElementEncoding)): - def andLibraries(libraries: ComponentLib*): Builder = copy(componentLibs = componentLibs ++ libraries) + case class ServerSideSessionBuilder(id: String, name: String, componentLibs: Seq[ComponentLib] = Seq(StdElementEncoding)): + def andLibraries(libraries: ComponentLib*): ServerSideSessionBuilder = copy(componentLibs = componentLibs ++ libraries) def connect[R](f: ConnectedSession => R): R = val config = Config.Default @@ -33,7 +33,7 @@ class ServerSideSessions(sessionsService: ServerSessionsService, executor: Fiber finally if !isStopped.get() && !connectedSession.isLeaveSessionOpen then sessionsService.terminateSession(session) - def withNewSession(id: String, name: String): Builder = Builder(id, name) + def withNewSession(id: String, name: String): ServerSideSessionBuilder = ServerSideSessionBuilder(id, name) trait ServerSideSessionsBeans: def sessionsService: ServerSessionsService From b5005a41a71acca777bf0cacc27a461b01933798 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 8 Feb 2024 14:20:08 +0000 Subject: [PATCH 011/313] - --- .../src/main/resources/logback.xml | 17 ++++++++++ .../serverapp/bundled/AppManager.scala | 5 --- .../serverapp/bundled/ServerStatusApp.scala | 31 ++++++++++++++++--- .../service/ServerSessionsService.scala | 4 +-- .../server/ui/SessionsWebSocket.scala | 2 +- .../client/components/chakra/QuickTable.scala | 5 ++- 6 files changed, 51 insertions(+), 13 deletions(-) create mode 100644 terminal21-server-app/src/main/resources/logback.xml diff --git a/terminal21-server-app/src/main/resources/logback.xml b/terminal21-server-app/src/main/resources/logback.xml new file mode 100644 index 00000000..17c2c6ad --- /dev/null +++ b/terminal21-server-app/src/main/resources/logback.xml @@ -0,0 +1,17 @@ + + + + + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala index dd94a979..4636084c 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala @@ -1,20 +1,15 @@ package org.terminal21.serverapp.bundled import functions.fibers.FiberExecutor -import org.slf4j.LoggerFactory import org.terminal21.client.ConnectedSession import org.terminal21.client.components.* import org.terminal21.client.components.chakra.* import org.terminal21.server.Dependencies -import org.terminal21.server.Terminal21Server.getClass import org.terminal21.serverapp.{ServerSideApp, ServerSideSessions} class AppManager(serverSideSessions: ServerSideSessions, fiberExecutor: FiberExecutor, apps: Seq[ServerSideApp], dependencies: Dependencies): - private val logger = LoggerFactory.getLogger(getClass) - def start(): Unit = fiberExecutor.submit: - logger.info("Starting AppManager") serverSideSessions .withNewSession("app-manager", "Apps") .connect: session => diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 173a2f03..0794e2aa 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -1,5 +1,6 @@ package org.terminal21.serverapp.bundled +import functions.fibers.FiberExecutor import org.terminal21.client.ConnectedSession import org.terminal21.client.components.* import org.terminal21.client.components.chakra.* @@ -17,14 +18,36 @@ class ServerStatusApp extends ServerSideApp: .withNewSession("server-status", "Server Status") .connect: session => given ConnectedSession = session - new ServerStatusAppInternal(dependencies.sessionsService).run() + new ServerStatusAppInternal(dependencies.sessionsService, dependencies.fiberExecutor).run() -private class ServerStatusAppInternal(sessionsService: ServerSessionsService)(using session: ConnectedSession): +class ServerStatusAppInternal(sessionsService: ServerSessionsService, executor: FiberExecutor)(using session: ConnectedSession): def run(): Unit = - updateStatus() + executor.submit: + while !session.isClosed do + updateStatus() + Thread.sleep(1000) session.waitTillUserClosesSession() + private def toMb(v: Long) = s"${v / (1024 * 1024)} MB" + private def updateStatus(): Unit = + val runtime = Runtime.getRuntime + + val jvmTable = QuickTable(caption = Some("JVM")) + .headers("Property", "Value", "Actions") + .rows( + Seq( + Seq("Free Memory", toMb(runtime.freeMemory()), ""), + Seq("Max Memory", toMb(runtime.maxMemory()), ""), + Seq( + "Total Memory", + toMb(runtime.totalMemory()), + Button(size = Some("2xs"), text = "Run GC").onClick: () => + System.gc() + ), + Seq("Available processors", runtime.availableProcessors(), "") + ) + ) val sessions = sessionsService.allSessions val sessionsTable = QuickTable( caption = Some("All sessions"), @@ -33,7 +56,7 @@ private class ServerStatusAppInternal(sessionsService: ServerSessionsService)(us ) .headers("Id", "Name", "Is Open", "Actions") - Seq(sessionsTable).render() + Seq(jvmTable, sessionsTable).render() private def actionsFor(session: Session)(using ConnectedSession): UiElement = if session.isOpen then diff --git a/terminal21-server/src/main/scala/org/terminal21/server/service/ServerSessionsService.scala b/terminal21-server/src/main/scala/org/terminal21/server/service/ServerSessionsService.scala index 434e3510..dd99e1f9 100644 --- a/terminal21-server/src/main/scala/org/terminal21/server/service/ServerSessionsService.scala +++ b/terminal21-server/src/main/scala/org/terminal21/server/service/ServerSessionsService.scala @@ -60,14 +60,14 @@ class ServerSessionsService extends SessionsService: val newV = oldV.withNewState(newStateJson) sessions += session -> newV sessionStateChangeNotificationRegistry.notifyAll((session, newV, None)) - logger.info(s"Session $session new state $newStateJson") + logger.debug(s"Session $session new state $newStateJson") override def changeSessionJsonState(session: Session, change: ServerJson): Unit = val oldV = sessions(session) val newV = oldV.withNewState(oldV.serverJson.include(change)) sessions += session -> newV sessionStateChangeNotificationRegistry.notifyAll((session, newV, Some(change))) - logger.info(s"Session $session change $change") + logger.debug(s"Session $session change $change") def triggerUiEvent(event: UiEvent): Unit = val e = event match diff --git a/terminal21-server/src/main/scala/org/terminal21/server/ui/SessionsWebSocket.scala b/terminal21-server/src/main/scala/org/terminal21/server/ui/SessionsWebSocket.scala index e78a3544..2d5f8615 100644 --- a/terminal21-server/src/main/scala/org/terminal21/server/ui/SessionsWebSocket.scala +++ b/terminal21-server/src/main/scala/org/terminal21/server/ui/SessionsWebSocket.scala @@ -60,7 +60,7 @@ class SessionsWebSocket(sessionsService: ServerSessionsService) extends WsListen logger.info(s"$wsSession: Received event $eventName = $event") sessionsService.triggerUiEvent(event) case Right(WsRequest("ping", None)) => - logger.info(s"$wsSession: ping received") + logger.debug(s"$wsSession: ping received") case Right(WsRequest("close-session", Some(CloseSession(sessionId)))) => val session = sessionsService.sessionById(sessionId) sessionsService.terminateSession(session) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala index a3394b89..7a8127ac 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala @@ -41,7 +41,10 @@ case class QuickTable( def headers(headers: String*): QuickTable = copy(headers = headers.map(h => Text(text = h))) def headersElements(headers: UiElement*): QuickTable = copy(headers = headers) - def rows(data: Seq[Seq[Any]]): QuickTable = copy(rows = data.map(_.map(c => Text(text = c.toString)))) + def rows(data: Seq[Seq[Any]]): QuickTable = copy(rows = data.map(_.map: + case u: UiElement => u + case c => Text(text = c.toString) + )) def rowsElements(data: Seq[Seq[UiElement]]): QuickTable = copy(rows = data) def caption(text: String): QuickTable = copy(caption = Some(text)) From bdbdc16fa2ec4cb59ee4daddcd7835cff4967ee9 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 8 Feb 2024 15:07:45 +0000 Subject: [PATCH 012/313] - --- .../main/scala/org/terminal21/server/ui/SessionsWebSocket.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal21-server/src/main/scala/org/terminal21/server/ui/SessionsWebSocket.scala b/terminal21-server/src/main/scala/org/terminal21/server/ui/SessionsWebSocket.scala index 2d5f8615..6e300558 100644 --- a/terminal21-server/src/main/scala/org/terminal21/server/ui/SessionsWebSocket.scala +++ b/terminal21-server/src/main/scala/org/terminal21/server/ui/SessionsWebSocket.scala @@ -45,7 +45,7 @@ class SessionsWebSocket(sessionsService: ServerSessionsService) extends WsListen wsSession.send(json, true) override def onMessage(wsSession: WsSession, text: String, last: Boolean): Unit = - logger.info(s"$wsSession: Received json: $text , last = $last") + logger.debug(s"$wsSession: Received json: $text , last = $last") errorLogger.logErrors: WsRequest.decoder(text) match case Right(WsRequest("sessions", None)) => From f794a2c482727e55f0dddf31a1d5cc39770f8796 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 8 Feb 2024 17:17:55 +0000 Subject: [PATCH 013/313] - --- .../main/scala/tests/ChakraComponents.scala | 50 +++++----- .../main/scala/tests/MathJaxComponents.scala | 31 +++--- .../src/main/scala/tests/NivoComponents.scala | 11 ++- .../src/main/scala/tests/StdComponents.scala | 54 ++++++----- .../terminal21/sparklib/SparkSessions.scala | 21 ---- .../sparklib/endtoend/SparkBasics.scala | 96 ++++++++++--------- .../org/terminal21/client/Sessions.scala | 61 ++++++------ 7 files changed, 163 insertions(+), 161 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala index 328ed5b6..74447147 100644 --- a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala @@ -14,27 +14,29 @@ import java.util.concurrent.atomic.AtomicBoolean while keepRunning.get() do println("Starting new session") - Sessions.withNewSession("chakra-components", "Chakra Components"): session => - keepRunning.set(false) - given ConnectedSession = session - - val latch = new CountDownLatch(1) - - // react tests reset the session to clear state - val krButton = Button(text = "Keep Running").onClick: () => - keepRunning.set(true) - latch.countDown() - - (Overlay.components ++ Forms.components ++ Editables.components ++ Stacks.components ++ Grids.components ++ Buttons.components( - latch - ) ++ Etc.components ++ MediaAndIcons.components ++ DataDisplay.components ++ Typography.components ++ Feedback.components ++ Disclosure.components ++ Navigation.components ++ Seq( - krButton - )) - .render() - - println("Waiting for button to be pressed for 1 hour") - session.waitTillUserClosesSessionOr(latch.getCount == 0) - if !session.isClosed then - session.clear() - Paragraph(text = "Terminated").render() - Thread.sleep(1000) + Sessions + .withNewSession("chakra-components", "Chakra Components") + .connect: session => + keepRunning.set(false) + given ConnectedSession = session + + val latch = new CountDownLatch(1) + + // react tests reset the session to clear state + val krButton = Button(text = "Keep Running").onClick: () => + keepRunning.set(true) + latch.countDown() + + (Overlay.components ++ Forms.components ++ Editables.components ++ Stacks.components ++ Grids.components ++ Buttons.components( + latch + ) ++ Etc.components ++ MediaAndIcons.components ++ DataDisplay.components ++ Typography.components ++ Feedback.components ++ Disclosure.components ++ Navigation.components ++ Seq( + krButton + )) + .render() + + println("Waiting for button to be pressed for 1 hour") + session.waitTillUserClosesSessionOr(latch.getCount == 0) + if !session.isClosed then + session.clear() + Paragraph(text = "Terminated").render() + Thread.sleep(1000) diff --git a/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala b/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala index 731e437a..5c2465ca 100644 --- a/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala @@ -6,17 +6,20 @@ import org.terminal21.client.components.chakra.* import org.terminal21.client.components.mathjax.* @main def mathJaxComponents(): Unit = - Sessions.withNewSession("mathjax-components", "MathJax Components", MathJaxLib): session => - given ConnectedSession = session - Seq( - HStack().withChildren( - Text(text = "Lets write some math expressions that will wow everybody!"), - MathJax(expression = """\[\sum_{n = 200}^{1000}\left(\frac{20\sqrt{n}}{n}\right)\]""") - ), - MathJax(expression = """Everyone knows this one : \(ax^2 + bx + c = 0\). But how about this? \(\sum_{i=1}^n i^3 = ((n(n+1))/2)^2 \)"""), - MathJax( - expression = """Does it align correctly? \(ax^2 + bx + c = 0\) It does provided CHTML renderer is used.""", - style = Map("backgroundColor" -> "gray") - ) - ).render() - session.leaveSessionOpenAfterExiting() + Sessions + .withNewSession("mathjax-components", "MathJax Components") + .andLibraries(MathJaxLib) + .connect: session => + given ConnectedSession = session + Seq( + HStack().withChildren( + Text(text = "Lets write some math expressions that will wow everybody!"), + MathJax(expression = """\[\sum_{n = 200}^{1000}\left(\frac{20\sqrt{n}}{n}\right)\]""") + ), + MathJax(expression = """Everyone knows this one : \(ax^2 + bx + c = 0\). But how about this? \(\sum_{i=1}^n i^3 = ((n(n+1))/2)^2 \)"""), + MathJax( + expression = """Does it align correctly? \(ax^2 + bx + c = 0\) It does provided CHTML renderer is used.""", + style = Map("backgroundColor" -> "gray") + ) + ).render() + session.leaveSessionOpenAfterExiting() diff --git a/end-to-end-tests/src/main/scala/tests/NivoComponents.scala b/end-to-end-tests/src/main/scala/tests/NivoComponents.scala index 2eb2aaa6..c861cc3c 100644 --- a/end-to-end-tests/src/main/scala/tests/NivoComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/NivoComponents.scala @@ -6,7 +6,10 @@ import org.terminal21.client.components.* import tests.nivo.{ResponsiveBarChart, ResponsiveLineChart} @main def nivoComponents(): Unit = - Sessions.withNewSession("nivo-components", "Nivo Components", NivoLib): session => - given ConnectedSession = session - (ResponsiveBarChart() ++ ResponsiveLineChart()).render() - session.waitTillUserClosesSession() + Sessions + .withNewSession("nivo-components", "Nivo Components") + .andLibraries(NivoLib) + .connect: session => + given ConnectedSession = session + (ResponsiveBarChart() ++ ResponsiveLineChart()).render() + session.waitTillUserClosesSession() diff --git a/end-to-end-tests/src/main/scala/tests/StdComponents.scala b/end-to-end-tests/src/main/scala/tests/StdComponents.scala index a822856f..4150ae0a 100644 --- a/end-to-end-tests/src/main/scala/tests/StdComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/StdComponents.scala @@ -5,32 +5,34 @@ import org.terminal21.client.components.* import org.terminal21.client.components.std.* @main def stdComponents(): Unit = - Sessions.withNewSession("std-components", "Std Components"): session => - given ConnectedSession = session + Sessions + .withNewSession("std-components", "Std Components") + .connect: session => + given ConnectedSession = session - val input = Input(defaultValue = Some("Please enter your name")) - val output = Paragraph(text = "This will reflect what you type in the input") - input.onChange: newValue => - output.withText(newValue).renderChanges() + val input = Input(defaultValue = Some("Please enter your name")) + val output = Paragraph(text = "This will reflect what you type in the input") + input.onChange: newValue => + output.withText(newValue).renderChanges() - Seq( - Header1(text = "header1 test"), - Header2(text = "header2 test"), - Header3(text = "header3 test"), - Header4(text = "header4 test"), - Header5(text = "header5 test"), - Header6(text = "header6 test"), - Paragraph(text = "Hello World!").withChildren( - NewLine(), - Span(text = "Some more text"), - Em(text = " emphasized!"), - NewLine(), - Span(text = "And the last line") - ), - Paragraph(text = "A Form ").withChildren( - input - ), - output - ).render() + Seq( + Header1(text = "header1 test"), + Header2(text = "header2 test"), + Header3(text = "header3 test"), + Header4(text = "header4 test"), + Header5(text = "header5 test"), + Header6(text = "header6 test"), + Paragraph(text = "Hello World!").withChildren( + NewLine(), + Span(text = "Some more text"), + Em(text = " emphasized!"), + NewLine(), + Span(text = "And the last line") + ), + Paragraph(text = "A Form ").withChildren( + input + ), + output + ).render() - session.waitTillUserClosesSession() + session.waitTillUserClosesSession() diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/SparkSessions.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/SparkSessions.scala index f7306ac1..7bb3cb3d 100644 --- a/terminal21-spark/src/main/scala/org/terminal21/sparklib/SparkSessions.scala +++ b/terminal21-spark/src/main/scala/org/terminal21/sparklib/SparkSessions.scala @@ -20,24 +20,3 @@ object SparkSessions: .config("spark.driver.bindAddress", bindAddress) .config("spark.ui.enabled", sparkUiEnabled) .getOrCreate() - - /** Will create a terminal21 session and use the provided spark session - * @param spark - * the spark session, will be closed before this call returns. Use #newSparkSession to quickly create one. - * @param id - * the id of the terminal21 session - * @param name - * the name of the terminal21 session - * @param f - * the code to run - * @tparam R - * if f returns some value, this will be returned by the method - * @return - * whatever f returns - */ - def newTerminal21WithSparkSession[R](spark: SparkSession, id: String, name: String, componentLibs: ComponentLib*)( - f: (SparkSession, ConnectedSession) => R - ): R = - Sessions.withNewSession(id, name, componentLibs: _*): terminal21Session => - Using.resource(spark): _ => - f(spark, terminal21Session) diff --git a/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala b/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala index ec6d1a2a..46f910ba 100644 --- a/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala +++ b/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala @@ -10,63 +10,69 @@ import org.terminal21.sparklib.* import org.terminal21.sparklib.endtoend.model.CodeFile import org.terminal21.sparklib.endtoend.model.CodeFile.scanSourceFiles +import scala.util.Using + @main def sparkBasics(): Unit = - SparkSessions.newTerminal21WithSparkSession(SparkSessions.newSparkSession(), "spark-basics", "Spark Basics", NivoLib): (spark, session) => - given ConnectedSession = session - given SparkSession = spark - import scala3encoders.given - import spark.implicits.* + Using.resource(SparkSessions.newSparkSession()): spark => + Sessions + .withNewSession("spark-basics", "Spark Basics") + .andLibraries(NivoLib) + .connect: session => + given ConnectedSession = session + given SparkSession = spark + import scala3encoders.given + import spark.implicits.* - val headers = Seq("id", "name", "path", "numOfLines", "numOfWords", "createdDate", "timestamp") + val headers = Seq("id", "name", "path", "numOfLines", "numOfWords", "createdDate", "timestamp") - val sortedFilesTable = QuickTable().headers(headers: _*).caption("Files sorted by createdDate and numOfWords") - val codeFilesTable = QuickTable().headers(headers: _*).caption("Unsorted files") + val sortedFilesTable = QuickTable().headers(headers: _*).caption("Files sorted by createdDate and numOfWords") + val codeFilesTable = QuickTable().headers(headers: _*).caption("Unsorted files") - val sortedSourceFilesDS = sortedSourceFiles(sourceFiles()) - val sortedCalc = sortedSourceFilesDS.visualize("Sorted files", sortedFilesTable): results => - val tableRows = results.take(3).toList.map(_.toData) - sortedFilesTable.rows(tableRows) + val sortedSourceFilesDS = sortedSourceFiles(sourceFiles()) + val sortedCalc = sortedSourceFilesDS.visualize("Sorted files", sortedFilesTable): results => + val tableRows = results.take(3).toList.map(_.toData) + sortedFilesTable.rows(tableRows) - val codeFilesCalculation = sourceFiles().visualize("Code files", codeFilesTable): results => - val dt = results.take(3).toList - codeFilesTable.rows(dt.map(_.toData)) + val codeFilesCalculation = sourceFiles().visualize("Code files", codeFilesTable): results => + val dt = results.take(3).toList + codeFilesTable.rows(dt.map(_.toData)) - val sortedFilesTableDF = QuickTable().headers(headers: _*).caption("Files sorted by createdDate and numOfWords ASC and as DF") - val sortedCalcAsDF = sourceFiles() - .sort($"createdDate".asc, $"numOfWords".asc) - .toDF() - .visualize("Sorted files DF", sortedFilesTableDF): results => - val tableRows = results.take(4).toList - sortedFilesTableDF.rows(tableRows.toUiTable) + val sortedFilesTableDF = QuickTable().headers(headers: _*).caption("Files sorted by createdDate and numOfWords ASC and as DF") + val sortedCalcAsDF = sourceFiles() + .sort($"createdDate".asc, $"numOfWords".asc) + .toDF() + .visualize("Sorted files DF", sortedFilesTableDF): results => + val tableRows = results.take(4).toList + sortedFilesTableDF.rows(tableRows.toUiTable) - val chart = ResponsiveLine( - data = Seq( - Serie( - "Scala", + val chart = ResponsiveLine( data = Seq( - Datum("plane", 262), - Datum("helicopter", 26), - Datum("boat", 43) - ) + Serie( + "Scala", + data = Seq( + Datum("plane", 262), + Datum("helicopter", 26), + Datum("boat", 43) + ) + ) + ), + axisBottom = Some(Axis(legend = "Class", legendOffset = 36)), + axisLeft = Some(Axis(legend = "Count", legendOffset = -40)), + legends = Seq(Legend()) ) - ), - axisBottom = Some(Axis(legend = "Class", legendOffset = 36)), - axisLeft = Some(Axis(legend = "Count", legendOffset = -40)), - legends = Seq(Legend()) - ) - val sourceFileChart = sortedSourceFilesDS.visualize("Biggest Code Files", chart): results => - val data = results.take(10).map(cf => Datum(StringUtils.substringBeforeLast(cf.name, ".scala"), cf.numOfLines)).toList - chart.withData(Seq(Serie("Scala", data = data))) + val sourceFileChart = sortedSourceFilesDS.visualize("Biggest Code Files", chart): results => + val data = results.take(10).map(cf => Datum(StringUtils.substringBeforeLast(cf.name, ".scala"), cf.numOfLines)).toList + chart.withData(Seq(Serie("Scala", data = data))) - Seq( - codeFilesCalculation, - sortedCalc, - sortedCalcAsDF, - sourceFileChart - ).render() + Seq( + codeFilesCalculation, + sortedCalc, + sortedCalcAsDF, + sourceFileChart + ).render() - session.waitTillUserClosesSession() + session.waitTillUserClosesSession() def sourceFiles()(using spark: SparkSession) = import scala3encoders.given diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Sessions.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Sessions.scala index 41776398..16fd5dc3 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Sessions.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Sessions.scala @@ -9,32 +9,39 @@ import org.terminal21.config.Config import org.terminal21.ui.std.SessionsServiceCallerFactory import java.util.concurrent.atomic.AtomicBoolean +import scala.util.Using.Releasable object Sessions: - def withNewSession[R](id: String, name: String, componentLibs: ComponentLib*)(f: ConnectedSession => R): R = - val config = Config.Default - val serverUrl = s"http://${config.host}:${config.port}" - val client = WebClient.builder - .baseUri(serverUrl) - .build - val transport = new HelidonTransport(client) - val sessionsService = SessionsServiceCallerFactory.newHelidonJsonSessionsService(transport) - val session = sessionsService.createSession(id, name) - val wsClient = WsClient.builder - .baseUri(s"ws://${config.host}:${config.port}") - .build - - val isStopped = new AtomicBoolean(false) - def terminate(): Unit = - isStopped.set(true) - - val encoding = new UiElementEncoding(Seq(StdElementEncoding) ++ componentLibs) - val connectedSession = ConnectedSession(session, encoding, serverUrl, sessionsService, terminate) - FiberExecutor.withFiberExecutor: executor => - val listener = new ClientEventsWsListener(wsClient, connectedSession, executor) - listener.start() - - try f(connectedSession) - finally - if !isStopped.get() && !connectedSession.isLeaveSessionOpen then sessionsService.terminateSession(session) - listener.close() + case class SessionBuilder(id: String, name: String, componentLibs: Seq[ComponentLib] = Seq(StdElementEncoding)): + def andLibraries(libraries: ComponentLib*): SessionBuilder = copy(componentLibs = componentLibs ++ libraries) + + def connect[R](f: ConnectedSession => R): R = + val config = Config.Default + val serverUrl = s"http://${config.host}:${config.port}" + val client = WebClient.builder + .baseUri(serverUrl) + .build + val transport = new HelidonTransport(client) + val sessionsService = SessionsServiceCallerFactory.newHelidonJsonSessionsService(transport) + val session = sessionsService.createSession(id, name) + val wsClient = WsClient.builder + .baseUri(s"ws://${config.host}:${config.port}") + .build + + val isStopped = new AtomicBoolean(false) + + def terminate(): Unit = + isStopped.set(true) + + val encoding = new UiElementEncoding(Seq(StdElementEncoding) ++ componentLibs) + val connectedSession = ConnectedSession(session, encoding, serverUrl, sessionsService, terminate) + FiberExecutor.withFiberExecutor: executor => + val listener = new ClientEventsWsListener(wsClient, connectedSession, executor) + listener.start() + + try f(connectedSession) + finally + if !isStopped.get() && !connectedSession.isLeaveSessionOpen then sessionsService.terminateSession(session) + listener.close() + + def withNewSession(id: String, name: String): SessionBuilder = SessionBuilder(id, name) From 36421ac01f41b8294d5e96ef132ac82b06b27123 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 8 Feb 2024 17:20:19 +0000 Subject: [PATCH 014/313] - --- Readme.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Readme.md b/Readme.md index 24fa6e4d..64ef554c 100644 --- a/Readme.md +++ b/Readme.md @@ -164,6 +164,12 @@ all changes that may have occurred at the browser and all the changes we did on Please use the [discussions](https://github.com/kostaskougios/terminal21-restapi/discussions) of the project to post any questions, comments or ideas. # Changelog + +## Version 0.30 + +- apps can now run on the server +- session builders refactoring for more flexible creation of sessions + ## Version 0.21 - more std and chakra components like Alert, Progress, Tooltip, Tabs. From 2b037e9d286253d0bdc7fe3533e1e2084c8da32c Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 8 Feb 2024 17:21:03 +0000 Subject: [PATCH 015/313] - --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 64ef554c..081650ea 100644 --- a/Readme.md +++ b/Readme.md @@ -156,7 +156,7 @@ the state in the client scripts. terminal21 ui components are immutable from v0.20. Use `component.withX(...).renderChanges()` to modify a component and render it. Note that the original `component` is not changed. -Also when getting a value of i.e. an Input, use `myInput.current.value`. `current` makes sure we read the component with +Also, when getting a value of i.e. an Input, use `myInput.current.value`. `current` makes sure we read the component with all changes that may have occurred at the browser and all the changes we did on our script. # Need help? From 035b20ee7705aeb2e16346575a8e10acc4cb574c Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 9 Feb 2024 12:30:57 +0000 Subject: [PATCH 016/313] - --- .../src/main/scala/tests/StdComponents.scala | 3 ++- .../scala/org/terminal21/client/Sessions.scala | 1 - .../client/components/UiElementEncoding.scala | 3 ++- .../client/components/std/StdHttp.scala | 15 +++++++++++++++ 4 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala diff --git a/end-to-end-tests/src/main/scala/tests/StdComponents.scala b/end-to-end-tests/src/main/scala/tests/StdComponents.scala index 4150ae0a..7bb6f3e5 100644 --- a/end-to-end-tests/src/main/scala/tests/StdComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/StdComponents.scala @@ -32,7 +32,8 @@ import org.terminal21.client.components.std.* Paragraph(text = "A Form ").withChildren( input ), - output + output, + Cookie(name = "std-components-test-cookie", value = "test-cookie-value") ).render() session.waitTillUserClosesSession() diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Sessions.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Sessions.scala index 16fd5dc3..293f1ff6 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Sessions.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Sessions.scala @@ -9,7 +9,6 @@ import org.terminal21.config.Config import org.terminal21.ui.std.SessionsServiceCallerFactory import java.util.concurrent.atomic.AtomicBoolean -import scala.util.Using.Releasable object Sessions: case class SessionBuilder(id: String, name: String, componentLibs: Seq[ComponentLib] = Seq(StdElementEncoding)): diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElementEncoding.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElementEncoding.scala index 04993b10..32ec06a4 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElementEncoding.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElementEncoding.scala @@ -4,7 +4,7 @@ import io.circe.* import io.circe.generic.auto.* import io.circe.syntax.* import org.terminal21.client.components.chakra.{Box, CEJson, ChakraElement} -import org.terminal21.client.components.std.{StdEJson, StdElement} +import org.terminal21.client.components.std.{StdEJson, StdElement, StdHttp} class UiElementEncoding(libs: Seq[ComponentLib]): given uiElementEncoder: Encoder[UiElement] = @@ -35,3 +35,4 @@ object StdElementEncoding extends ComponentLib: case c: UiComponent => val b: ChakraElement[Box] = Box(key = c.key, text = "") b.asJson.mapObject(o => o.add("type", "Chakra".asJson)) + case std: StdHttp => std.asJson.mapObject(o => o.add("type", "Std".asJson)) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala new file mode 100644 index 00000000..e51edee2 --- /dev/null +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala @@ -0,0 +1,15 @@ +package org.terminal21.client.components.std + +import org.terminal21.client.components.{Keys, UiElement} + +/** Elements mapping to Http functionality + */ +sealed trait StdHttp extends UiElement + +case class Cookie( + key: String = Keys.nextKey, + name: String = "cookie.name", + value: String = "cookie.value", + path: Option[String] = None, + expireDays: Option[Int] = None +) extends StdHttp From 1d278c1ac08f94f8a642bb9a0bf7e4a8c707e0ca Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 9 Feb 2024 12:49:28 +0000 Subject: [PATCH 017/313] - --- .../main/scala/org/terminal21/client/components/NivoLib.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/terminal21-nivo/src/main/scala/org/terminal21/client/components/NivoLib.scala b/terminal21-nivo/src/main/scala/org/terminal21/client/components/NivoLib.scala index 4a68b032..5141c4d5 100644 --- a/terminal21-nivo/src/main/scala/org/terminal21/client/components/NivoLib.scala +++ b/terminal21-nivo/src/main/scala/org/terminal21/client/components/NivoLib.scala @@ -1,10 +1,9 @@ package org.terminal21.client.components +import io.circe.* import io.circe.generic.auto.* import io.circe.syntax.* -import io.circe.* import org.terminal21.client.components.nivo.NEJson -import org.terminal21.client.components.{ComponentLib, UiElement} object NivoLib extends ComponentLib: import org.terminal21.client.components.StdElementEncoding.given From 3f1c17b39084269cc772f5ba51deb39670a6d9a8 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 9 Feb 2024 19:09:18 +0000 Subject: [PATCH 018/313] - --- .../main/scala/tests/chakra/Disclosure.scala | 4 +- .../serverapp/bundled/ServerStatusApp.scala | 67 ++++++++++++++++--- .../components/chakra/ChakraElement.scala | 25 ++++--- .../client/components/chakra/QuickTable.scala | 1 + .../client/components/chakra/QuickTabs.scala | 33 +++++++++ .../client/components/std/StdHttp.scala | 2 + 6 files changed, 109 insertions(+), 23 deletions(-) create mode 100644 terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Disclosure.scala b/end-to-end-tests/src/main/scala/tests/chakra/Disclosure.scala index a048f372..27e1ca65 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Disclosure.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Disclosure.scala @@ -12,8 +12,8 @@ object Disclosure: commonBox(text = "Tabs"), Tabs().withChildren( TabList().withChildren( - Tab(text = "tab-one", _selected = Map("color" -> "white", "bg" -> "blue.500")), - Tab(text = "tab-two", _selected = Map("color" -> "white", "bg" -> "green.400")), + Tab(text = "tab-one").withSelected(Map("color" -> "white", "bg" -> "blue.500")), + Tab(text = "tab-two").withSelected(Map("color" -> "white", "bg" -> "green.400")), Tab(text = "tab-three") ), TabPanels().withChildren( diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 0794e2aa..6e09277a 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -6,6 +6,7 @@ import org.terminal21.client.components.* import org.terminal21.client.components.chakra.* import org.terminal21.model.Session import org.terminal21.server.Dependencies +import org.terminal21.server.model.SessionState import org.terminal21.server.service.ServerSessionsService import org.terminal21.serverapp.{ServerSideApp, ServerSideSessions} @@ -18,9 +19,11 @@ class ServerStatusApp extends ServerSideApp: .withNewSession("server-status", "Server Status") .connect: session => given ConnectedSession = session - new ServerStatusAppInternal(dependencies.sessionsService, dependencies.fiberExecutor).run() + new ServerStatusAppInternal(serverSideSessions, dependencies.sessionsService, dependencies.fiberExecutor).run() -class ServerStatusAppInternal(sessionsService: ServerSessionsService, executor: FiberExecutor)(using session: ConnectedSession): +class ServerStatusAppInternal(serverSideSessions: ServerSideSessions, sessionsService: ServerSessionsService, executor: FiberExecutor)(using + session: ConnectedSession +): def run(): Unit = executor.submit: while !session.isClosed do @@ -28,8 +31,8 @@ class ServerStatusAppInternal(sessionsService: ServerSessionsService, executor: Thread.sleep(1000) session.waitTillUserClosesSession() - private def toMb(v: Long) = s"${v / (1024 * 1024)} MB" - + private def toMb(v: Long) = s"${v / (1024 * 1024)} MB" + private val xs = Some("2xs") private def updateStatus(): Unit = val runtime = Runtime.getRuntime @@ -42,7 +45,7 @@ class ServerStatusAppInternal(sessionsService: ServerSessionsService, executor: Seq( "Total Memory", toMb(runtime.totalMemory()), - Button(size = Some("2xs"), text = "Run GC").onClick: () => + Button(size = xs, text = "Run GC").onClick: () => System.gc() ), Seq("Available processors", runtime.availableProcessors(), "") @@ -60,9 +63,53 @@ class ServerStatusAppInternal(sessionsService: ServerSessionsService, executor: private def actionsFor(session: Session)(using ConnectedSession): UiElement = if session.isOpen then - Button(text = "Close", size = Some("sm")) - .withLeftIcon(CloseIcon()) - .onClick: () => - sessionsService.terminateAndRemove(session) - updateStatus() + Box().withChildren( + Button(text = "Close", size = xs) + .withLeftIcon(SmallCloseIcon()) + .onClick: () => + sessionsService.terminateAndRemove(session) + updateStatus() + , + Text(text = " "), + Button(text = "View State", size = xs) + .withLeftIcon(ChatIcon()) + .onClick: () => + serverSideSessions + .withNewSession(session.id + "-server-state", s"Server State:${session.id}") + .connect: sSession => + new ViewServerState(sSession).runFor(sessionsService.sessionStateOf(session)) + ) else NotAllowedIcon() + +class ViewServerState(session: ConnectedSession): + given ConnectedSession = session + + def runFor(state: SessionState): Unit = + val sj = state.serverJson + + val rootKeyPanel = Seq( + QuickTable() + .withCaption("Root Keys") + .headers("Root Key") + .rows( + sj.rootKeys.map(k => Seq(k)) + ) + ) + + val keyTreePanel = Seq( + QuickTable() + .withCaption("Key Tree") + .headers("Key", "Children") + .rows( + sj.keyTree.map((k, v) => Seq(k, v.mkString(", "))).toSeq + ) + ) + Seq( + QuickTabs() + .withTabs("Root Keys", "Key Tree") + .withTabPanels( + rootKeyPanel, + keyTreePanel + ) + ).render() + session.waitTillUserClosesSession() diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala index 0c5ae15a..bf59c392 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala @@ -1839,21 +1839,24 @@ case class Tab( key: String = Keys.nextKey, text: String = "tab.text", isDisabled: Option[Boolean] = None, - _selected: Map[String, Any] = Map.empty, - _hover: Map[String, Any] = Map.empty, - _active: Map[String, Any] = Map.empty, + _selected: Option[Map[String, Any]] = None, + _hover: Option[Map[String, Any]] = None, + _active: Option[Map[String, Any]] = None, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil ) extends ChakraElement[Tab] with HasChildren[Tab]: - def withKey(v: String) = copy(key = v) - def withText(v: String) = copy(text = v) - override def withChildren(cn: UiElement*) = copy(children = cn) - override def withStyle(v: Map[String, Any]) = copy(style = v) - def withIsDisabled(v: Option[Boolean]) = copy(isDisabled = v) - def withSelected(v: Map[String, Any]) = copy(_selected = v) - def withHover(v: Map[String, Any]) = copy(_hover = v) - def withActive(v: Map[String, Any]) = copy(_active = v) + def withKey(v: String) = copy(key = v) + def withText(v: String) = copy(text = v) + override def withChildren(cn: UiElement*) = copy(children = cn) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withIsDisabled(v: Option[Boolean]) = copy(isDisabled = v) + def withSelected(v: Map[String, Any]) = copy(_selected = Some(v)) + def withSelected(v: Option[Map[String, Any]]) = copy(_selected = v) + def withHover(v: Map[String, Any]) = copy(_hover = Some(v)) + def withHover(v: Option[Map[String, Any]]) = copy(_hover = v) + def withActive(v: Map[String, Any]) = copy(_active = Some(v)) + def withActive(v: Option[Map[String, Any]]) = copy(_active = v) /** see https://chakra-ui.com/docs/components/tabs */ diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala index 7a8127ac..8ba731be 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala @@ -19,6 +19,7 @@ case class QuickTable( def withColorScheme(v: String) = copy(colorScheme = v) def withSize(v: String) = copy(size = v) def withCaption(v: Option[String]) = copy(caption = v) + def withCaption(v: String) = copy(caption = Some(v)) def withHeaders(v: Seq[UiElement]) = copy(headers = v) def withRows(v: Seq[Seq[UiElement]]) = copy(rows = v) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala new file mode 100644 index 00000000..cea409a9 --- /dev/null +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala @@ -0,0 +1,33 @@ +package org.terminal21.client.components.chakra + +import org.terminal21.client.components.UiElement.HasStyle +import org.terminal21.client.components.{Keys, UiComponent, UiElement} + +case class QuickTabs( + key: String = Keys.nextKey, + style: Map[String, Any] = Map.empty, + tabs: Seq[String | Seq[UiElement]] = Nil, + tabPanels: Seq[Seq[UiElement]] = Nil +) extends UiComponent + with HasStyle[QuickTabs]: + + def withTabs(tabs: String | Seq[UiElement]*): QuickTabs = copy(tabs = tabs) + def withTabPanels(tabPanels: Seq[UiElement]*): QuickTabs = copy(tabPanels = tabPanels) + + override lazy val rendered = Seq( + Tabs(key = key + "-tabs", style = style).withChildren( + TabList( + key = key + "-tab-list", + children = tabs.zipWithIndex.map: + case (name: String, idx) => Tab(key = s"$key-tab-$idx", text = name) + case (elements: Seq[UiElement], idx) => Tab(key = s"$key-tab-$idx", children = elements) + ), + TabPanels( + key = key + "-panels", + children = tabPanels.zipWithIndex.map: (elements, idx) => + TabPanel(key = s"$key-panel-$idx", children = elements) + ) + ) + ) + + override def withStyle(v: Map[String, Any]): QuickTabs = copy(style = v) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala index e51edee2..29901cae 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala @@ -6,6 +6,8 @@ import org.terminal21.client.components.{Keys, UiElement} */ sealed trait StdHttp extends UiElement +/** On the browser, https://github.com/js-cookie/js-cookie is used. + */ case class Cookie( key: String = Keys.nextKey, name: String = "cookie.name", From 15f49530935a1c25f6de7072cfb38c624e5d4165 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 9 Feb 2024 19:14:47 +0000 Subject: [PATCH 019/313] - --- .../org/terminal21/serverapp/bundled/ServerStatusApp.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 6e09277a..8cff3ff3 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -92,7 +92,7 @@ class ViewServerState(session: ConnectedSession): .withCaption("Root Keys") .headers("Root Key") .rows( - sj.rootKeys.map(k => Seq(k)) + sj.rootKeys.sorted.map(k => Seq(k)) ) ) @@ -101,7 +101,7 @@ class ViewServerState(session: ConnectedSession): .withCaption("Key Tree") .headers("Key", "Children") .rows( - sj.keyTree.map((k, v) => Seq(k, v.mkString(", "))).toSeq + sj.keyTree.toSeq.sortBy(_._1).map((k, v) => Seq(k, v.mkString(", "))) ) ) Seq( From 8dc6fce2572414a9410fe1f7165f005f97297ed0 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 9 Feb 2024 19:23:53 +0000 Subject: [PATCH 020/313] - --- .../serverapp/bundled/ServerStatusApp.scala | 17 +++++++++++++++-- .../server/ui/SessionsWebSocket.scala | 10 +++++----- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 8cff3ff3..41b43b33 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -104,12 +104,25 @@ class ViewServerState(session: ConnectedSession): sj.keyTree.toSeq.sortBy(_._1).map((k, v) => Seq(k, v.mkString(", "))) ) ) + + val componentJson = Seq( + QuickTable() + .withCaption("Component Json") + .headers("Key", "Json") + .rows( + sj.elements.toSeq + .sortBy(_._1) + .map: (k, j) => + Seq(k, j.noSpaces) + ) + ) Seq( QuickTabs() - .withTabs("Root Keys", "Key Tree") + .withTabs("Root Keys", "Key Tree", "Component Json") .withTabPanels( rootKeyPanel, - keyTreePanel + keyTreePanel, + componentJson ) ).render() session.waitTillUserClosesSession() diff --git a/terminal21-server/src/main/scala/org/terminal21/server/ui/SessionsWebSocket.scala b/terminal21-server/src/main/scala/org/terminal21/server/ui/SessionsWebSocket.scala index 6e300558..0a7507dd 100644 --- a/terminal21-server/src/main/scala/org/terminal21/server/ui/SessionsWebSocket.scala +++ b/terminal21-server/src/main/scala/org/terminal21/server/ui/SessionsWebSocket.scala @@ -30,18 +30,18 @@ class SessionsWebSocket(sessionsService: ServerSessionsService) extends WsListen private def sendSessionState(wsSession: WsSession, session: Session, sessionState: SessionState): Unit = val response = StateWsResponse(session.hideSecret, sessionState.serverJson).asJson.noSpaces - logger.info(s"$wsSession: Sending session state response $response") + logger.debug(s"$wsSession: Sending session state response $response") wsSession.send(response, true) private def sendSessionStateChange(wsSession: WsSession, session: Session, change: ServerJson): Unit = val response = StateChangeWsResponse(session.hideSecret, change).asJson.noSpaces - logger.info(s"$wsSession: Sending session change state response $response") + logger.debug(s"$wsSession: Sending session change state response $response") wsSession.send(response, true) private def sendSessions(wsSession: WsSession, allSessions: Seq[Session]): Unit = val sessions = allSessions.map(_.hideSecret).sortBy(_.name) val json = SessionsWsResponse(sessions).asJson.noSpaces - logger.info(s"$wsSession: Sending sessions $json") + logger.debug(s"$wsSession: Sending sessions $json") wsSession.send(json, true) override def onMessage(wsSession: WsSession, text: String, last: Boolean): Unit = @@ -50,14 +50,14 @@ class SessionsWebSocket(sessionsService: ServerSessionsService) extends WsListen WsRequest.decoder(text) match case Right(WsRequest("sessions", None)) => continuouslyRespond(wsSession) - logger.info(s"$wsSession: sessions processed successfully") + logger.debug(s"$wsSession: sessions processed successfully") case Right(WsRequest("session-full-refresh", Some(SessionFullRefresh(sessionId)))) => logger.info(s"$wsSession: session-full-refresh requested, sending full session data for $sessionId") val session = sessionsService.sessionById(sessionId) val sessionState = sessionsService.sessionStateOf(session) sendSessionState(wsSession, session, sessionState) case Right(WsRequest(eventName, Some(event: UiEvent))) => - logger.info(s"$wsSession: Received event $eventName = $event") + logger.debug(s"$wsSession: Received event $eventName = $event") sessionsService.triggerUiEvent(event) case Right(WsRequest("ping", None)) => logger.debug(s"$wsSession: ping received") From 63e947b3dd01aa532540b0d0618dc70a104bdb3f Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 9 Feb 2024 20:33:16 +0000 Subject: [PATCH 021/313] - --- Readme.md | 3 ++- .../serverapp/bundled/ServerStatusApp.scala | 20 ++++--------------- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/Readme.md b/Readme.md index 081650ea..b8881889 100644 --- a/Readme.md +++ b/Readme.md @@ -167,8 +167,9 @@ Please use the [discussions](https://github.com/kostaskougios/terminal21-restapi ## Version 0.30 -- apps can now run on the server +- apps can now run on the server + server management bundled apps - session builders refactoring for more flexible creation of sessions +- QuickTabs ## Version 0.21 diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 41b43b33..b40563b1 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -99,30 +99,18 @@ class ViewServerState(session: ConnectedSession): val keyTreePanel = Seq( QuickTable() .withCaption("Key Tree") - .headers("Key", "Children") + .headers("Key", "Component Json", "Children") .rows( - sj.keyTree.toSeq.sortBy(_._1).map((k, v) => Seq(k, v.mkString(", "))) + sj.keyTree.toSeq.sortBy(_._1).map((k, v) => Seq(k, sj.elements(k).noSpaces, v.mkString(", "))) ) ) - val componentJson = Seq( - QuickTable() - .withCaption("Component Json") - .headers("Key", "Json") - .rows( - sj.elements.toSeq - .sortBy(_._1) - .map: (k, j) => - Seq(k, j.noSpaces) - ) - ) Seq( QuickTabs() - .withTabs("Root Keys", "Key Tree", "Component Json") + .withTabs("Root Keys", "Key Tree") .withTabPanels( rootKeyPanel, - keyTreePanel, - componentJson + keyTreePanel ) ).render() session.waitTillUserClosesSession() From 360f0da33493659605434dc192a82cdeae8b646c Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Sun, 11 Feb 2024 20:58:11 +0000 Subject: [PATCH 022/313] - --- docs/tutorial.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial.md b/docs/tutorial.md index 7da7ad8d..e81d3cea 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -15,7 +15,7 @@ This tutorial will use `scala-cli` but the same applies for `sbt` or `mill` proj have `scala-cli` installed on your box, you're good to go, there are no other requirements to run terminal21 scripts. Jdk and dependencies will be downloaded by `scala-cli` for us. -All example code is under `example-scripts` of this repo, feel free to check the repo and run them. +All example code is under `example-scripts` of this repo, feel free to checkout the repo and run them. ## Starting the terminal21 server From d989bc9e4cc1a55f42cf8a33098a28639d8406cb Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 12 Feb 2024 14:30:07 +0000 Subject: [PATCH 023/313] - --- .../client/components/TransientRequest.scala | 6 ++++++ .../client/components/std/StdHttp.scala | 16 +++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 terminal21-ui-std/src/main/scala/org/terminal21/client/components/TransientRequest.scala diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/TransientRequest.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/TransientRequest.scala new file mode 100644 index 00000000..4d45f2a3 --- /dev/null +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/TransientRequest.scala @@ -0,0 +1,6 @@ +package org.terminal21.client.components + +import java.util.UUID + +object TransientRequest: + def newRequestId(): String = UUID.randomUUID().toString diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala index 29901cae..6ea8ba62 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala @@ -1,10 +1,19 @@ package org.terminal21.client.components.std -import org.terminal21.client.components.{Keys, UiElement} +import org.terminal21.client.components.{Keys, TransientRequest, UiElement} /** Elements mapping to Http functionality */ -sealed trait StdHttp extends UiElement +sealed trait StdHttp extends UiElement: + /** Each requestId will be processed only once per browser. + * + * I.e. lets say we have the Cookie(). If we add a cookie, we send it to the UI which in turn checks if it already set the cookie via the requestId. If it + * did, it skips it, if it didn't it sets the cookie. + * + * @return + * Should always be TransientRequest.newRequestId() + */ + def requestId: String /** On the browser, https://github.com/js-cookie/js-cookie is used. */ @@ -13,5 +22,6 @@ case class Cookie( name: String = "cookie.name", value: String = "cookie.value", path: Option[String] = None, - expireDays: Option[Int] = None + expireDays: Option[Int] = None, + requestId: String = TransientRequest.newRequestId() ) extends StdHttp From e2c896555b71827357e771a796b524331ee73e89 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 12 Feb 2024 14:54:20 +0000 Subject: [PATCH 024/313] - --- .../src/main/scala/tests/StdComponents.scala | 10 +++++++--- .../client/components/std/StdHttp.scala | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/StdComponents.scala b/end-to-end-tests/src/main/scala/tests/StdComponents.scala index 7bb6f3e5..ee8192b4 100644 --- a/end-to-end-tests/src/main/scala/tests/StdComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/StdComponents.scala @@ -10,8 +10,9 @@ import org.terminal21.client.components.std.* .connect: session => given ConnectedSession = session - val input = Input(defaultValue = Some("Please enter your name")) - val output = Paragraph(text = "This will reflect what you type in the input") + val input = Input(defaultValue = Some("Please enter your name")) + val output = Paragraph(text = "This will reflect what you type in the input") + val cookieValue = Paragraph(text = "This will display the value of the cookie") input.onChange: newValue => output.withText(newValue).renderChanges() @@ -33,7 +34,10 @@ import org.terminal21.client.components.std.* input ), output, - Cookie(name = "std-components-test-cookie", value = "test-cookie-value") + Cookie(name = "std-components-test-cookie", value = "test-cookie-value"), + CookieReader(name = "std-components-test-cookie").onChange: newValue => + cookieValue.withText(s"Cookie value $newValue").renderChanges(), + cookieValue ).render() session.waitTillUserClosesSession() diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala index 6ea8ba62..904b1e60 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala @@ -1,6 +1,9 @@ package org.terminal21.client.components.std +import org.terminal21.client.{ConnectedSession, EventHandler, OnChangeEventHandler} +import org.terminal21.client.components.UiElement.HasEventHandler import org.terminal21.client.components.{Keys, TransientRequest, UiElement} +import org.terminal21.model.OnChange /** Elements mapping to Http functionality */ @@ -25,3 +28,16 @@ case class Cookie( expireDays: Option[Int] = None, requestId: String = TransientRequest.newRequestId() ) extends StdHttp + +case class CookieReader( + key: String = Keys.nextKey, + name: String = "cookie.name", + value: Option[String] = None, // will be set when/if cookie value is read + requestId: String = TransientRequest.newRequestId() +) extends StdHttp + with HasEventHandler: + override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = newValue => session.modified(copy(value = Some(newValue))) + + def onChange(h: OnChangeEventHandler)(using session: ConnectedSession): CookieReader = + session.addEventHandler(key, h) + this From 2707a83a4224ddbc7619faf5aac2dda04b0960fa Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 12 Feb 2024 18:10:38 +0000 Subject: [PATCH 025/313] - --- .../scala/org/terminal21/client/components/std/StdHttp.scala | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala index 904b1e60..9d01fc98 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala @@ -19,6 +19,8 @@ sealed trait StdHttp extends UiElement: def requestId: String /** On the browser, https://github.com/js-cookie/js-cookie is used. + * + * Set a cookie on the browser. */ case class Cookie( key: String = Keys.nextKey, @@ -29,6 +31,8 @@ case class Cookie( requestId: String = TransientRequest.newRequestId() ) extends StdHttp +/** Read a cookie value. The value, when read from the ui, it will reflect in `value`. Also the onChange handler will be called once with the value. + */ case class CookieReader( key: String = Keys.nextKey, name: String = "cookie.name", From b47ca88796ca866c43f4b1452fb8c5318db3a332 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 13 Feb 2024 12:00:32 +0000 Subject: [PATCH 026/313] - --- .../serverapp/bundled/DefaultApps.scala | 3 +- .../serverapp/bundled/SettingsApp.scala | 37 +++++++++++++++++++ .../client/components/UiElementEncoding.scala | 10 +++-- .../components/chakra/ChakraElement.scala | 4 ++ .../client/components/std/StdElement.scala | 2 +- .../client/components/std/StdHttp.scala | 3 +- .../components/ui/FrontEndElement.scala | 7 ++++ 7 files changed, 59 insertions(+), 7 deletions(-) create mode 100644 terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala create mode 100644 terminal21-ui-std/src/main/scala/org/terminal21/client/components/ui/FrontEndElement.scala diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/DefaultApps.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/DefaultApps.scala index e9bcc63a..101c49ba 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/DefaultApps.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/DefaultApps.scala @@ -2,5 +2,6 @@ package org.terminal21.serverapp.bundled object DefaultApps: val All = Seq( - new ServerStatusApp + new ServerStatusApp, + new SettingsApp ) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala new file mode 100644 index 00000000..c1c5b16a --- /dev/null +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala @@ -0,0 +1,37 @@ +package org.terminal21.serverapp.bundled + +import org.terminal21.client.ConnectedSession +import org.terminal21.client.components.ui.ThemeToggle +import org.terminal21.client.components.* +import org.terminal21.client.components.chakra.{Link, Text} +import org.terminal21.client.components.std.Paragraph +import org.terminal21.server.Dependencies +import org.terminal21.serverapp.{ServerSideApp, ServerSideSessions} + +class SettingsApp extends ServerSideApp: + override def name = "Settings" + + override def description = "Settings for terminal21" + + override def createSession(serverSideSessions: ServerSideSessions, dependencies: Dependencies): Unit = + serverSideSessions + .withNewSession("frontend-settings", "Settings") + .connect: session => + given ConnectedSession = session + new SettingsAppInstance().run() + +class SettingsAppInstance(using session: ConnectedSession): + def run() = + Seq( + ThemeToggle(), + Paragraph().withChildren( + Text(text = "Have a question? Please ask at "), + Link( + text = "terminal21's discussion board", + href = "https://github.com/kostaskougios/terminal21-restapi/discussions", + color = Some("teal.500"), + isExternal = Some(true) + ) + ) + ).render() + session.waitTillUserClosesSession() diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElementEncoding.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElementEncoding.scala index 32ec06a4..97f6f37b 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElementEncoding.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElementEncoding.scala @@ -5,6 +5,7 @@ import io.circe.generic.auto.* import io.circe.syntax.* import org.terminal21.client.components.chakra.{Box, CEJson, ChakraElement} import org.terminal21.client.components.std.{StdEJson, StdElement, StdHttp} +import org.terminal21.client.components.ui.FrontEndElement class UiElementEncoding(libs: Seq[ComponentLib]): given uiElementEncoder: Encoder[UiElement] = @@ -30,9 +31,10 @@ object StdElementEncoding extends ComponentLib: Json.obj(vs: _*) override def toJson(using Encoder[UiElement]): PartialFunction[UiElement, Json] = - case std: StdEJson => std.asJson.mapObject(o => o.add("type", "Std".asJson)) - case c: CEJson => c.asJson.mapObject(o => o.add("type", "Chakra".asJson)) - case c: UiComponent => + case std: StdEJson => std.asJson.mapObject(o => o.add("type", "Std".asJson)) + case c: CEJson => c.asJson.mapObject(o => o.add("type", "Chakra".asJson)) + case c: UiComponent => val b: ChakraElement[Box] = Box(key = c.key, text = "") b.asJson.mapObject(o => o.add("type", "Chakra".asJson)) - case std: StdHttp => std.asJson.mapObject(o => o.add("type", "Std".asJson)) + case std: StdHttp => std.asJson.mapObject(o => o.add("type", "Std".asJson)) + case fe: FrontEndElement => fe.asJson.mapObject(o => o.add("type", "FrontEnd".asJson)) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala index bf59c392..61e1e96c 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala @@ -1940,6 +1940,7 @@ case class Link( text: String = "link.text", href: String = "#", isExternal: Option[Boolean] = None, + color: Option[String] = None, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil ) extends ChakraElement[Link] @@ -1949,5 +1950,8 @@ case class Link( override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withIsExternal(v: Option[Boolean]) = copy(isExternal = v) + def withIsExternal(v: Boolean) = copy(isExternal = Some(v)) def withHref(v: String) = copy(href = v) def withText(v: String) = copy(text = v) + def withColor(v: String) = copy(color = Some(v)) + def withColor(v: Option[String]) = copy(color = v) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala index bf1b967c..8ee72beb 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala @@ -53,7 +53,7 @@ case class Header6(key: String = Keys.nextKey, text: String, style: Map[String, case class Paragraph( key: String = Keys.nextKey, - text: String = "paragraph.text", + text: String = "", style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil ) extends StdElement[Paragraph] diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala index 9d01fc98..03376e86 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala @@ -31,7 +31,8 @@ case class Cookie( requestId: String = TransientRequest.newRequestId() ) extends StdHttp -/** Read a cookie value. The value, when read from the ui, it will reflect in `value`. Also the onChange handler will be called once with the value. +/** Read a cookie value. The value, when read from the ui, it will reflect in `value` assuming the UI had the time to send the value back. Also the onChange + * handler will be called once with the value. */ case class CookieReader( key: String = Keys.nextKey, diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/ui/FrontEndElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/ui/FrontEndElement.scala new file mode 100644 index 00000000..7e337316 --- /dev/null +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/ui/FrontEndElement.scala @@ -0,0 +1,7 @@ +package org.terminal21.client.components.ui + +import org.terminal21.client.components.{Keys, UiElement} + +sealed trait FrontEndElement extends UiElement + +case class ThemeToggle(key: String = Keys.nextKey) extends FrontEndElement From 0677aa817022090a73a40c082fde89dfc2bccae6 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 13 Feb 2024 12:08:25 +0000 Subject: [PATCH 027/313] - --- .../org/terminal21/serverapp/bundled/SettingsApp.scala | 10 +++++----- .../client/components/chakra/ChakraElement.scala | 2 ++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala index c1c5b16a..15d63127 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala @@ -3,8 +3,8 @@ package org.terminal21.serverapp.bundled import org.terminal21.client.ConnectedSession import org.terminal21.client.components.ui.ThemeToggle import org.terminal21.client.components.* -import org.terminal21.client.components.chakra.{Link, Text} -import org.terminal21.client.components.std.Paragraph +import org.terminal21.client.components.chakra.{ExternalLinkIcon, Link, Text} +import org.terminal21.client.components.std.{Paragraph, Span} import org.terminal21.server.Dependencies import org.terminal21.serverapp.{ServerSideApp, ServerSideSessions} @@ -25,13 +25,13 @@ class SettingsAppInstance(using session: ConnectedSession): Seq( ThemeToggle(), Paragraph().withChildren( - Text(text = "Have a question? Please ask at "), + Span(text = "Have a question? Please ask at "), Link( - text = "terminal21's discussion board", + text = "terminal21's discussion board ", href = "https://github.com/kostaskougios/terminal21-restapi/discussions", color = Some("teal.500"), isExternal = Some(true) - ) + ).withChildren(ExternalLinkIcon(mx = Some("2px"))) ) ).render() session.waitTillUserClosesSession() diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala index 61e1e96c..3a4d5c03 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala @@ -836,6 +836,7 @@ case class ExternalLinkIcon( key: String = Keys.nextKey, w: Option[String] = None, h: Option[String] = None, + mx: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty @@ -846,6 +847,7 @@ case class ExternalLinkIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + def withMx(v: Option[String]) = copy(mx = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ From 9ae46b16cafb653ef92f39cf15e93a179ac45ffb Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 13 Feb 2024 12:10:35 +0000 Subject: [PATCH 028/313] - --- .../org/terminal21/serverapp/bundled/SettingsApp.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala index 15d63127..c00fed55 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala @@ -1,10 +1,10 @@ package org.terminal21.serverapp.bundled import org.terminal21.client.ConnectedSession -import org.terminal21.client.components.ui.ThemeToggle import org.terminal21.client.components.* -import org.terminal21.client.components.chakra.{ExternalLinkIcon, Link, Text} +import org.terminal21.client.components.chakra.{ExternalLinkIcon, Link} import org.terminal21.client.components.std.{Paragraph, Span} +import org.terminal21.client.components.ui.ThemeToggle import org.terminal21.server.Dependencies import org.terminal21.serverapp.{ServerSideApp, ServerSideSessions} @@ -24,7 +24,7 @@ class SettingsAppInstance(using session: ConnectedSession): def run() = Seq( ThemeToggle(), - Paragraph().withChildren( + Paragraph(style = Map("margin" -> "25px")).withChildren( Span(text = "Have a question? Please ask at "), Link( text = "terminal21's discussion board ", From d1e4dd9e3e008f0485d4fa6c66be91637449c905 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 13 Feb 2024 12:16:01 +0000 Subject: [PATCH 029/313] - --- .../scala/org/terminal21/serverapp/bundled/AppManager.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala index 4636084c..753d4878 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala @@ -11,7 +11,7 @@ class AppManager(serverSideSessions: ServerSideSessions, fiberExecutor: FiberExe def start(): Unit = fiberExecutor.submit: serverSideSessions - .withNewSession("app-manager", "Apps") + .withNewSession("app-manager", "Terminal 21") .connect: session => given ConnectedSession = session From 2d3b804a7495c1f0b79c295e5301bc29c1bff18e Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 13 Feb 2024 12:34:38 +0000 Subject: [PATCH 030/313] - --- .../org/terminal21/serverapp/ServerSideSessions.scala | 11 +++++++++-- .../terminal21/serverapp/bundled/SettingsApp.scala | 2 ++ .../terminal21/serverapp/ServerSideSessionsTest.scala | 10 +++++----- .../src/main/scala/org/terminal21/model/Session.scala | 2 +- .../scala/org/terminal21/model/SessionOptions.scala | 6 ++++++ .../org/terminal21/model/CommonModelBuilders.scala | 10 ++++++++-- .../server/service/ServerSessionsService.scala | 5 +++-- .../server/service/ServerSessionsServiceTest.scala | 4 ++-- .../scala/org/terminal21/ui/std/SessionsService.scala | 4 ++-- .../main/scala/org/terminal21/client/Sessions.scala | 11 +++++++++-- 10 files changed, 47 insertions(+), 18 deletions(-) create mode 100644 terminal21-server-client-common/src/main/scala/org/terminal21/model/SessionOptions.scala diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideSessions.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideSessions.scala index 93dd9dcf..413932f9 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideSessions.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideSessions.scala @@ -4,19 +4,26 @@ import functions.fibers.FiberExecutor import org.terminal21.client.ConnectedSession import org.terminal21.client.components.{ComponentLib, StdElementEncoding, UiElementEncoding} import org.terminal21.config.Config +import org.terminal21.model.SessionOptions import org.terminal21.server.service.ServerSessionsService import java.util.concurrent.atomic.AtomicBoolean class ServerSideSessions(sessionsService: ServerSessionsService, executor: FiberExecutor): - case class ServerSideSessionBuilder(id: String, name: String, componentLibs: Seq[ComponentLib] = Seq(StdElementEncoding)): + case class ServerSideSessionBuilder( + id: String, + name: String, + componentLibs: Seq[ComponentLib] = Seq(StdElementEncoding), + sessionOptions: SessionOptions = SessionOptions.Defaults + ): def andLibraries(libraries: ComponentLib*): ServerSideSessionBuilder = copy(componentLibs = componentLibs ++ libraries) + def andOptions(sessionOptions: SessionOptions) = copy(sessionOptions = sessionOptions) def connect[R](f: ConnectedSession => R): R = val config = Config.Default val serverUrl = s"http://${config.host}:${config.port}" - val session = sessionsService.createSession(id, name) + val session = sessionsService.createSession(id, name, sessionOptions) val encoding = new UiElementEncoding(Seq(StdElementEncoding) ++ componentLibs) val isStopped = new AtomicBoolean(false) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala index c00fed55..a67c0db2 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala @@ -5,6 +5,7 @@ import org.terminal21.client.components.* import org.terminal21.client.components.chakra.{ExternalLinkIcon, Link} import org.terminal21.client.components.std.{Paragraph, Span} import org.terminal21.client.components.ui.ThemeToggle +import org.terminal21.model.SessionOptions import org.terminal21.server.Dependencies import org.terminal21.serverapp.{ServerSideApp, ServerSideSessions} @@ -16,6 +17,7 @@ class SettingsApp extends ServerSideApp: override def createSession(serverSideSessions: ServerSideSessions, dependencies: Dependencies): Unit = serverSideSessions .withNewSession("frontend-settings", "Settings") + .andOptions(SessionOptions(deleteWhenTerminated = true)) .connect: session => given ConnectedSession = session new SettingsAppInstance().run() diff --git a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/ServerSideSessionsTest.scala b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/ServerSideSessionsTest.scala index bbaa603e..5db136d1 100644 --- a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/ServerSideSessionsTest.scala +++ b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/ServerSideSessionsTest.scala @@ -6,7 +6,7 @@ import org.mockito.Mockito.{verify, when} import org.scalatest.BeforeAndAfterAll import org.scalatest.funsuite.AnyFunSuiteLike import org.scalatestplus.mockito.MockitoSugar.* -import org.terminal21.model.CommonModelBuilders +import org.terminal21.model.{CommonModelBuilders, SessionOptions} import org.terminal21.model.CommonModelBuilders.session import org.terminal21.server.service.ServerSessionsService @@ -17,18 +17,18 @@ class ServerSideSessionsTest extends AnyFunSuiteLike with BeforeAndAfterAll: test("creates session"): new App: val s = session() - when(sessionsService.createSession(s.id, s.name)).thenReturn(s) + when(sessionsService.createSession(s.id, s.name, SessionOptions.Defaults)).thenReturn(s) serverSideSessions .withNewSession(s.id, s.name) .connect: session => session.leaveSessionOpenAfterExiting() - verify(sessionsService).createSession(s.id, s.name) + verify(sessionsService).createSession(s.id, s.name, SessionOptions.Defaults) test("terminates session before exiting"): new App: val s = session() - when(sessionsService.createSession(s.id, s.name)).thenReturn(s) + when(sessionsService.createSession(s.id, s.name, SessionOptions.Defaults)).thenReturn(s) serverSideSessions .withNewSession(s.id, s.name) .connect: _ => @@ -39,7 +39,7 @@ class ServerSideSessionsTest extends AnyFunSuiteLike with BeforeAndAfterAll: test("registers to receive events"): new App: val s = session() - when(sessionsService.createSession(s.id, s.name)).thenReturn(s) + when(sessionsService.createSession(s.id, s.name, SessionOptions.Defaults)).thenReturn(s) serverSideSessions .withNewSession(s.id, s.name) .connect: _ => diff --git a/terminal21-server-client-common/src/main/scala/org/terminal21/model/Session.scala b/terminal21-server-client-common/src/main/scala/org/terminal21/model/Session.scala index 05db6f42..62d2a613 100644 --- a/terminal21-server-client-common/src/main/scala/org/terminal21/model/Session.scala +++ b/terminal21-server-client-common/src/main/scala/org/terminal21/model/Session.scala @@ -1,5 +1,5 @@ package org.terminal21.model -case class Session(id: String, name: String, secret: String, isOpen: Boolean): +case class Session(id: String, name: String, secret: String, isOpen: Boolean, sessionOptions: SessionOptions): def hideSecret: Session = copy(secret = "***") def close: Session = copy(isOpen = false) diff --git a/terminal21-server-client-common/src/main/scala/org/terminal21/model/SessionOptions.scala b/terminal21-server-client-common/src/main/scala/org/terminal21/model/SessionOptions.scala new file mode 100644 index 00000000..38b96359 --- /dev/null +++ b/terminal21-server-client-common/src/main/scala/org/terminal21/model/SessionOptions.scala @@ -0,0 +1,6 @@ +package org.terminal21.model + +case class SessionOptions(deleteWhenTerminated: Boolean = false) + +object SessionOptions: + val Defaults = SessionOptions() diff --git a/terminal21-server-client-common/src/test/scala/org/terminal21/model/CommonModelBuilders.scala b/terminal21-server-client-common/src/test/scala/org/terminal21/model/CommonModelBuilders.scala index a78db392..c5cd69de 100644 --- a/terminal21-server-client-common/src/test/scala/org/terminal21/model/CommonModelBuilders.scala +++ b/terminal21-server-client-common/src/test/scala/org/terminal21/model/CommonModelBuilders.scala @@ -1,5 +1,11 @@ package org.terminal21.model object CommonModelBuilders: - def session(id: String = "session-id", name: String = "session-name", secret: String = "session-secret", isOpen: Boolean = true) = - Session(id, name, secret, isOpen) + def session( + id: String = "session-id", + name: String = "session-name", + secret: String = "session-secret", + isOpen: Boolean = true, + sessionOptions: SessionOptions = SessionOptions.Defaults + ) = + Session(id, name, secret, isOpen, sessionOptions) diff --git a/terminal21-server/src/main/scala/org/terminal21/server/service/ServerSessionsService.scala b/terminal21-server/src/main/scala/org/terminal21/server/service/ServerSessionsService.scala index dd99e1f9..bbeb1eb4 100644 --- a/terminal21-server/src/main/scala/org/terminal21/server/service/ServerSessionsService.scala +++ b/terminal21-server/src/main/scala/org/terminal21/server/service/ServerSessionsService.scala @@ -35,13 +35,14 @@ class ServerSessionsService extends SessionsService: sessions -= session sessions += session.close -> state.close sessionChangeNotificationRegistry.notifyAll(allSessions) + if (session.sessionOptions.deleteWhenTerminated) removeSession(session.close) def terminateAndRemove(session: Session): Unit = terminateSession(session) removeSession(session.close) - override def createSession(id: String, name: String): Session = - val s = Session(id, name, UUID.randomUUID().toString, true) + override def createSession(id: String, name: String, sessionOptions: SessionOptions): Session = + val s = Session(id, name, UUID.randomUUID().toString, true, sessionOptions) logger.info(s"Creating session $s") sessions.keys.toList.foreach(s => if s.id == id then sessions.remove(s)) val state = SessionState(ServerJson.Empty, new NotificationRegistry) diff --git a/terminal21-server/src/test/scala/org/terminal21/server/service/ServerSessionsServiceTest.scala b/terminal21-server/src/test/scala/org/terminal21/server/service/ServerSessionsServiceTest.scala index ddd96db5..b7477401 100644 --- a/terminal21-server/src/test/scala/org/terminal21/server/service/ServerSessionsServiceTest.scala +++ b/terminal21-server/src/test/scala/org/terminal21/server/service/ServerSessionsServiceTest.scala @@ -3,7 +3,7 @@ package org.terminal21.server.service import io.circe.Json import org.scalatest.funsuite.AnyFunSuiteLike import org.scalatest.matchers.should.Matchers.* -import org.terminal21.model.{OnChange, OnClick, SessionClosed} +import org.terminal21.model.{OnChange, OnClick, SessionClosed, SessionOptions} import org.terminal21.server.json import org.terminal21.ui.std.StdExportsBuilders.serverJson @@ -147,4 +147,4 @@ class ServerSessionsServiceTest extends AnyFunSuiteLike: class App: val serverSessionsService = new ServerSessionsService - def createSession = serverSessionsService.createSession("test", "Test") + def createSession = serverSessionsService.createSession("test", "Test", SessionOptions.Defaults) diff --git a/terminal21-ui-std-exports/src/main/scala/org/terminal21/ui/std/SessionsService.scala b/terminal21-ui-std-exports/src/main/scala/org/terminal21/ui/std/SessionsService.scala index 8809d7da..98e09b85 100644 --- a/terminal21-ui-std-exports/src/main/scala/org/terminal21/ui/std/SessionsService.scala +++ b/terminal21-ui-std-exports/src/main/scala/org/terminal21/ui/std/SessionsService.scala @@ -1,11 +1,11 @@ package org.terminal21.ui.std -import org.terminal21.model.Session +import org.terminal21.model.{Session, SessionOptions} /** //> exported */ trait SessionsService: - def createSession(id: String, name: String): Session + def createSession(id: String, name: String, sessionOptions: SessionOptions): Session def terminateSession(session: Session): Unit def setSessionJsonState(session: Session, state: ServerJson): Unit diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Sessions.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Sessions.scala index 293f1ff6..94309c73 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Sessions.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Sessions.scala @@ -6,13 +6,20 @@ import io.helidon.webclient.api.WebClient import io.helidon.webclient.websocket.WsClient import org.terminal21.client.components.{ComponentLib, StdElementEncoding, UiElementEncoding} import org.terminal21.config.Config +import org.terminal21.model.SessionOptions import org.terminal21.ui.std.SessionsServiceCallerFactory import java.util.concurrent.atomic.AtomicBoolean object Sessions: - case class SessionBuilder(id: String, name: String, componentLibs: Seq[ComponentLib] = Seq(StdElementEncoding)): + case class SessionBuilder( + id: String, + name: String, + componentLibs: Seq[ComponentLib] = Seq(StdElementEncoding), + sessionOptions: SessionOptions = SessionOptions.Defaults + ): def andLibraries(libraries: ComponentLib*): SessionBuilder = copy(componentLibs = componentLibs ++ libraries) + def andOptions(sessionOptions: SessionOptions) = copy(sessionOptions = sessionOptions) def connect[R](f: ConnectedSession => R): R = val config = Config.Default @@ -22,7 +29,7 @@ object Sessions: .build val transport = new HelidonTransport(client) val sessionsService = SessionsServiceCallerFactory.newHelidonJsonSessionsService(transport) - val session = sessionsService.createSession(id, name) + val session = sessionsService.createSession(id, name, sessionOptions) val wsClient = WsClient.builder .baseUri(s"ws://${config.host}:${config.port}") .build From 15aa2ea24538eb5fcf28d7ace7a21ec05514293b Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 13 Feb 2024 12:38:31 +0000 Subject: [PATCH 031/313] - --- .../service/ServerSessionsServiceTest.scala | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/terminal21-server/src/test/scala/org/terminal21/server/service/ServerSessionsServiceTest.scala b/terminal21-server/src/test/scala/org/terminal21/server/service/ServerSessionsServiceTest.scala index b7477401..2c78f58b 100644 --- a/terminal21-server/src/test/scala/org/terminal21/server/service/ServerSessionsServiceTest.scala +++ b/terminal21-server/src/test/scala/org/terminal21/server/service/ServerSessionsServiceTest.scala @@ -10,20 +10,20 @@ import org.terminal21.ui.std.StdExportsBuilders.serverJson class ServerSessionsServiceTest extends AnyFunSuiteLike: test("sessionById"): new App: - val session = createSession + val session = createSession() serverSessionsService.setSessionJsonState(session, serverJson()) serverSessionsService.sessionById(session.id) should be(session) test("sessionStateOf"): new App: - val session = createSession + val session = createSession() val sj = serverJson() serverSessionsService.setSessionJsonState(session, sj) serverSessionsService.sessionStateOf(session).serverJson should be(sj) test("removeSession"): new App: - val session = createSession + val session = createSession() serverSessionsService.setSessionJsonState(session, serverJson()) serverSessionsService.removeSession(session) an[IllegalArgumentException] should be thrownBy: @@ -31,7 +31,7 @@ class ServerSessionsServiceTest extends AnyFunSuiteLike: test("removeSession notifies listeners"): new App: - val session = createSession + val session = createSession() serverSessionsService.setSessionJsonState(session, serverJson()) var listenerCalled = 0 serverSessionsService.notifyMeWhenSessionsChange: sessions => @@ -45,14 +45,21 @@ class ServerSessionsServiceTest extends AnyFunSuiteLike: test("terminateSession marks session as closed"): new App: - val session = createSession + val session = createSession() serverSessionsService.setSessionJsonState(session, serverJson()) serverSessionsService.terminateSession(session) serverSessionsService.sessionById(session.id).isOpen should be(false) + test("terminateSession removes session if marked to be deleted when terminated"): + new App: + val session = createSession(SessionOptions(deleteWhenTerminated = true)) + serverSessionsService.setSessionJsonState(session, serverJson()) + serverSessionsService.terminateSession(session) + serverSessionsService.allSessions should be(Nil) + test("terminateSession notifies session listeners"): new App: - val session = createSession + val session = createSession() serverSessionsService.setSessionJsonState(session, serverJson()) var eventCalled = false serverSessionsService.notifyMeOnSessionEvents(session): event => @@ -64,7 +71,7 @@ class ServerSessionsServiceTest extends AnyFunSuiteLike: test("terminateSession notifies sessions listeners"): new App: - val session = createSession + val session = createSession() serverSessionsService.setSessionJsonState(session, serverJson()) var listenerCalled = 0 serverSessionsService.notifyMeWhenSessionsChange: sessions => @@ -86,12 +93,12 @@ class ServerSessionsServiceTest extends AnyFunSuiteLike: listenerCalled += 1 true - createSession + createSession() listenerCalled should be(2) test("changeSessionJsonState changes session's state"): new App: - val session = createSession + val session = createSession() val sj1 = serverJson(elements = Map("e1" -> Json.fromString("e1v"))) serverSessionsService.setSessionJsonState(session, sj1) val sj2 = serverJson(elements = Map("e2" -> Json.fromString("e2v"))) @@ -102,7 +109,7 @@ class ServerSessionsServiceTest extends AnyFunSuiteLike: test("changeSessionJsonState notifies listeners"): new App: - val session = createSession + val session = createSession() val sj1 = serverJson(elements = Map("e1" -> Json.fromString("e1v"))) serverSessionsService.setSessionJsonState(session, sj1) val sj2 = serverJson(elements = Map("e2" -> Json.fromString("e2v"))) @@ -123,7 +130,7 @@ class ServerSessionsServiceTest extends AnyFunSuiteLike: test("triggerUiEvent notifies listeners for clicks"): new App: - val session = createSession + val session = createSession() var called = false serverSessionsService.notifyMeOnSessionEvents(session): e => called = true @@ -135,7 +142,7 @@ class ServerSessionsServiceTest extends AnyFunSuiteLike: test("triggerUiEvent notifies listeners for change"): new App: - val session = createSession + val session = createSession() var called = false serverSessionsService.notifyMeOnSessionEvents(session): e => called = true @@ -146,5 +153,5 @@ class ServerSessionsServiceTest extends AnyFunSuiteLike: called should be(true) class App: - val serverSessionsService = new ServerSessionsService - def createSession = serverSessionsService.createSession("test", "Test", SessionOptions.Defaults) + val serverSessionsService = new ServerSessionsService + def createSession(options: SessionOptions = SessionOptions.Defaults) = serverSessionsService.createSession("test", "Test", options) From 4a9348295f32ed20a86921469bfee88b4843f18e Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 13 Feb 2024 12:40:26 +0000 Subject: [PATCH 032/313] - --- .../scala/org/terminal21/serverapp/bundled/SettingsApp.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala index a67c0db2..c3b5e5d2 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala @@ -12,7 +12,7 @@ import org.terminal21.serverapp.{ServerSideApp, ServerSideSessions} class SettingsApp extends ServerSideApp: override def name = "Settings" - override def description = "Settings for terminal21" + override def description = "Terminal21 Settings" override def createSession(serverSideSessions: ServerSideSessions, dependencies: Dependencies): Unit = serverSideSessions From 3c2664119f13120ccbe230420f526240fbb0fa18 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 13 Feb 2024 12:57:56 +0000 Subject: [PATCH 033/313] - --- .../scala/org/terminal21/serverapp/bundled/AppManager.scala | 2 ++ .../org/terminal21/serverapp/bundled/ServerStatusApp.scala | 3 ++- .../scala/org/terminal21/serverapp/bundled/SettingsApp.scala | 2 +- .../src/main/scala/org/terminal21/model/Session.scala | 2 +- .../src/main/scala/org/terminal21/model/SessionOptions.scala | 2 +- .../org/terminal21/server/service/ServerSessionsService.scala | 2 +- .../terminal21/server/service/ServerSessionsServiceTest.scala | 2 +- 7 files changed, 9 insertions(+), 6 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala index 753d4878..f0a56ce5 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala @@ -4,6 +4,7 @@ import functions.fibers.FiberExecutor import org.terminal21.client.ConnectedSession import org.terminal21.client.components.* import org.terminal21.client.components.chakra.* +import org.terminal21.model.SessionOptions import org.terminal21.server.Dependencies import org.terminal21.serverapp.{ServerSideApp, ServerSideSessions} @@ -12,6 +13,7 @@ class AppManager(serverSideSessions: ServerSideSessions, fiberExecutor: FiberExe fiberExecutor.submit: serverSideSessions .withNewSession("app-manager", "Terminal 21") + .andOptions(SessionOptions(alwaysOpen = true)) .connect: session => given ConnectedSession = session diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index b40563b1..77cf5329 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -4,7 +4,7 @@ import functions.fibers.FiberExecutor import org.terminal21.client.ConnectedSession import org.terminal21.client.components.* import org.terminal21.client.components.chakra.* -import org.terminal21.model.Session +import org.terminal21.model.{Session, SessionOptions} import org.terminal21.server.Dependencies import org.terminal21.server.model.SessionState import org.terminal21.server.service.ServerSessionsService @@ -17,6 +17,7 @@ class ServerStatusApp extends ServerSideApp: override def createSession(serverSideSessions: ServerSideSessions, dependencies: Dependencies): Unit = serverSideSessions .withNewSession("server-status", "Server Status") + .andOptions(SessionOptions(closeTabWhenTerminated = true)) .connect: session => given ConnectedSession = session new ServerStatusAppInternal(serverSideSessions, dependencies.sessionsService, dependencies.fiberExecutor).run() diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala index c3b5e5d2..ebf356f9 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala @@ -17,7 +17,7 @@ class SettingsApp extends ServerSideApp: override def createSession(serverSideSessions: ServerSideSessions, dependencies: Dependencies): Unit = serverSideSessions .withNewSession("frontend-settings", "Settings") - .andOptions(SessionOptions(deleteWhenTerminated = true)) + .andOptions(SessionOptions(closeTabWhenTerminated = true)) .connect: session => given ConnectedSession = session new SettingsAppInstance().run() diff --git a/terminal21-server-client-common/src/main/scala/org/terminal21/model/Session.scala b/terminal21-server-client-common/src/main/scala/org/terminal21/model/Session.scala index 62d2a613..dd05660d 100644 --- a/terminal21-server-client-common/src/main/scala/org/terminal21/model/Session.scala +++ b/terminal21-server-client-common/src/main/scala/org/terminal21/model/Session.scala @@ -1,5 +1,5 @@ package org.terminal21.model -case class Session(id: String, name: String, secret: String, isOpen: Boolean, sessionOptions: SessionOptions): +case class Session(id: String, name: String, secret: String, isOpen: Boolean, options: SessionOptions): def hideSecret: Session = copy(secret = "***") def close: Session = copy(isOpen = false) diff --git a/terminal21-server-client-common/src/main/scala/org/terminal21/model/SessionOptions.scala b/terminal21-server-client-common/src/main/scala/org/terminal21/model/SessionOptions.scala index 38b96359..b8cf76da 100644 --- a/terminal21-server-client-common/src/main/scala/org/terminal21/model/SessionOptions.scala +++ b/terminal21-server-client-common/src/main/scala/org/terminal21/model/SessionOptions.scala @@ -1,6 +1,6 @@ package org.terminal21.model -case class SessionOptions(deleteWhenTerminated: Boolean = false) +case class SessionOptions(closeTabWhenTerminated: Boolean = false, alwaysOpen: Boolean = false) object SessionOptions: val Defaults = SessionOptions() diff --git a/terminal21-server/src/main/scala/org/terminal21/server/service/ServerSessionsService.scala b/terminal21-server/src/main/scala/org/terminal21/server/service/ServerSessionsService.scala index bbeb1eb4..5aed7c89 100644 --- a/terminal21-server/src/main/scala/org/terminal21/server/service/ServerSessionsService.scala +++ b/terminal21-server/src/main/scala/org/terminal21/server/service/ServerSessionsService.scala @@ -35,7 +35,7 @@ class ServerSessionsService extends SessionsService: sessions -= session sessions += session.close -> state.close sessionChangeNotificationRegistry.notifyAll(allSessions) - if (session.sessionOptions.deleteWhenTerminated) removeSession(session.close) + if (session.options.closeTabWhenTerminated) removeSession(session.close) def terminateAndRemove(session: Session): Unit = terminateSession(session) diff --git a/terminal21-server/src/test/scala/org/terminal21/server/service/ServerSessionsServiceTest.scala b/terminal21-server/src/test/scala/org/terminal21/server/service/ServerSessionsServiceTest.scala index 2c78f58b..f2bae7e5 100644 --- a/terminal21-server/src/test/scala/org/terminal21/server/service/ServerSessionsServiceTest.scala +++ b/terminal21-server/src/test/scala/org/terminal21/server/service/ServerSessionsServiceTest.scala @@ -52,7 +52,7 @@ class ServerSessionsServiceTest extends AnyFunSuiteLike: test("terminateSession removes session if marked to be deleted when terminated"): new App: - val session = createSession(SessionOptions(deleteWhenTerminated = true)) + val session = createSession(SessionOptions(closeTabWhenTerminated = true)) serverSessionsService.setSessionJsonState(session, serverJson()) serverSessionsService.terminateSession(session) serverSessionsService.allSessions should be(Nil) From 0e7426ba92564186e860bd0771f7551d0cbd5f04 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 13 Feb 2024 15:15:43 +0000 Subject: [PATCH 034/313] - --- .../org/terminal21/serverapp/bundled/ServerStatusApp.scala | 3 ++- .../src/main/scala/org/terminal21/model/SessionOptions.scala | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 77cf5329..b0f33e2e 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -16,7 +16,7 @@ class ServerStatusApp extends ServerSideApp: override def createSession(serverSideSessions: ServerSideSessions, dependencies: Dependencies): Unit = serverSideSessions - .withNewSession("server-status", "Server Status") + .withNewSession("server-status", "ZServer Status") .andOptions(SessionOptions(closeTabWhenTerminated = true)) .connect: session => given ConnectedSession = session @@ -77,6 +77,7 @@ class ServerStatusAppInternal(serverSideSessions: ServerSideSessions, sessionsSe .onClick: () => serverSideSessions .withNewSession(session.id + "-server-state", s"Server State:${session.id}") + .andOptions(SessionOptions.CloseTabWhenTerminated) .connect: sSession => new ViewServerState(sSession).runFor(sessionsService.sessionStateOf(session)) ) diff --git a/terminal21-server-client-common/src/main/scala/org/terminal21/model/SessionOptions.scala b/terminal21-server-client-common/src/main/scala/org/terminal21/model/SessionOptions.scala index b8cf76da..eb7a8821 100644 --- a/terminal21-server-client-common/src/main/scala/org/terminal21/model/SessionOptions.scala +++ b/terminal21-server-client-common/src/main/scala/org/terminal21/model/SessionOptions.scala @@ -3,4 +3,5 @@ package org.terminal21.model case class SessionOptions(closeTabWhenTerminated: Boolean = false, alwaysOpen: Boolean = false) object SessionOptions: - val Defaults = SessionOptions() + val Defaults = SessionOptions() + val CloseTabWhenTerminated = SessionOptions(closeTabWhenTerminated = true) From cedf9ee0386b054e38786cbf196971cd4cbb42a1 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 13 Feb 2024 17:28:08 +0000 Subject: [PATCH 035/313] - --- .../org/terminal21/serverapp/bundled/ServerStatusApp.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index b0f33e2e..3ff78f77 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -16,7 +16,7 @@ class ServerStatusApp extends ServerSideApp: override def createSession(serverSideSessions: ServerSideSessions, dependencies: Dependencies): Unit = serverSideSessions - .withNewSession("server-status", "ZServer Status") + .withNewSession("server-status", "Server Status") .andOptions(SessionOptions(closeTabWhenTerminated = true)) .connect: session => given ConnectedSession = session From e60025261ad7a9f3bdf9109c18600e889ae04b19 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 15 Feb 2024 11:40:46 +0000 Subject: [PATCH 036/313] - --- example-scripts/bouncing-ball.sc | 31 +++--- example-scripts/csv-editor.sc | 96 +++++++++--------- example-scripts/csv-viewer.sc | 42 ++++---- example-scripts/hello-world.sc | 10 +- example-scripts/mathjax.sc | 25 ++--- example-scripts/nivo-bar-chart.sc | 73 +++++++------- example-scripts/nivo-line-chart.sc | 43 ++++---- example-scripts/on-change.sc | 34 ++++--- example-scripts/on-click.sc | 20 ++-- example-scripts/postit.sc | 60 ++++++------ example-scripts/progress.sc | 38 +++---- example-scripts/project.scala | 6 +- example-scripts/read-changed-value.sc | 40 ++++---- example-scripts/server.sc | 2 +- example-scripts/textedit.sc | 98 ++++++++++--------- .../serverapp/bundled/ServerStatusApp.scala | 1 - .../org/terminal21/model/SessionOptions.scala | 6 +- .../service/ServerSessionsService.scala | 1 + .../service/ServerSessionsServiceTest.scala | 12 ++- 19 files changed, 339 insertions(+), 299 deletions(-) diff --git a/example-scripts/bouncing-ball.sc b/example-scripts/bouncing-ball.sc index 11b491fb..e06824eb 100755 --- a/example-scripts/bouncing-ball.sc +++ b/example-scripts/bouncing-ball.sc @@ -8,26 +8,29 @@ // always import these import org.terminal21.client.* import org.terminal21.client.components.* +import org.terminal21.model.SessionOptions // use the chakra components for menus, forms etc, https://chakra-ui.com/docs/components // The scala case classes : https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala import org.terminal21.client.components.chakra.* import scala.annotation.tailrec -Sessions.withNewSession("bouncing-ball", "C64 bouncing ball"): session => - given ConnectedSession = session +Sessions + .withNewSession("bouncing-ball", "C64 bouncing ball") + .connect: session => + given ConnectedSession = session - println( - "Files under ~/.terminal21/web will be served under /web . Please place a ball.png file under ~/.terminal21/web/images on the box where the server runs." - ) - val ball = Image(src = "/web/images/ball.png") - ball.render() + println( + "Files under ~/.terminal21/web will be served under /web . Please place a ball.png file under ~/.terminal21/web/images on the box where the server runs." + ) + val ball = Image(src = "/web/images/ball.png") + ball.render() - @tailrec def animateBall(x: Int, y: Int, dx: Int, dy: Int): Unit = - ball.withStyle("position" -> "fixed", "left" -> (x + "px"), "top" -> (y + "px")).renderChanges() - Thread.sleep(1000 / 120) - val newDx = if x < 0 || x > 600 then -dx else dx - val newDy = if y < 0 || y > 500 then -dy else dy - if !session.isClosed then animateBall(x + newDx, y + newDy, newDx, newDy) + @tailrec def animateBall(x: Int, y: Int, dx: Int, dy: Int): Unit = + ball.withStyle("position" -> "fixed", "left" -> (x + "px"), "top" -> (y + "px")).renderChanges() + Thread.sleep(1000 / 120) + val newDx = if x < 0 || x > 600 then -dx else dx + val newDy = if y < 0 || y > 500 then -dy else dy + if !session.isClosed then animateBall(x + newDx, y + newDy, newDx, newDy) - animateBall(50, 50, 8, 8) + animateBall(50, 50, 8, 8) diff --git a/example-scripts/csv-editor.sc b/example-scripts/csv-editor.sc index 75abf9cb..92bb11e6 100755 --- a/example-scripts/csv-editor.sc +++ b/example-scripts/csv-editor.sc @@ -9,6 +9,7 @@ import org.terminal21.client.* import java.util.concurrent.atomic.AtomicBoolean import org.terminal21.client.components.* +import org.terminal21.model.SessionOptions // use the chakra components for menus, forms etc, https://chakra-ui.com/docs/components // The scala case classes : https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala import org.terminal21.client.components.chakra.* @@ -28,6 +29,7 @@ val contents = if file.exists() then FileUtils.readFileToString(file, "UTF-8") else "type,damage points,hit points\nmage,10dp,20hp\nwarrior,20dp,30hp" +println(s"Contents: $contents") val csv = contents.split("\n").map(_.split(",")) // store the csv data in a more usable Map @@ -57,52 +59,54 @@ def saveCsvMap() = // this will be set to true when we have to exit val exitFlag = new AtomicBoolean(false) -Sessions.withNewSession(s"csv-editor-$fileName", s"CsvEdit: $fileName"): session => - given ConnectedSession = session - - val status = Box() - val saveAndExit = Button(text = "Save & Exit") - .onClick: () => - saveCsvMap() - status.withText("Csv file saved, exiting.").renderChanges() - exitFlag.set(true) - - val exit = Button(text = "Exit Without Saving") - .onClick: () => - exitFlag.set(true) - - def newEditable(x: Int, y: Int, value: String) = - Editable(defaultValue = value) - .withChildren( - EditablePreview(), - EditableInput() - ) - .onChange: newValue => - csvMap((x, y)) = newValue - status.withText(s"($x,$y) value changed to $newValue").renderChanges() +Sessions + .withNewSession(s"csv-editor-$fileName", s"CsvEdit: $fileName") + .connect: session => + given ConnectedSession = session + + val status = Box() + val saveAndExit = Button(text = "Save & Exit") + .onClick: () => + saveCsvMap() + status.withText("Csv file saved, exiting.").renderChanges() + exitFlag.set(true) - Seq( - TableContainer().withChildren( - Table(variant = "striped", colorScheme = Some("teal"), size = "mg") + val exit = Button(text = "Exit Without Saving") + .onClick: () => + exitFlag.set(true) + + def newEditable(x: Int, y: Int, value: String) = + Editable(defaultValue = value) .withChildren( - TableCaption(text = "Please edit the csv contents above and click save to save and exit"), - Thead(), - Tbody( - children = csv.zipWithIndex.map: (row, y) => - Tr( - children = row.zipWithIndex.map: (column, x) => - Td().withChildren(newEditable(x, y, column)) - ) - ) + EditablePreview(), + EditableInput() ) - ), - HStack().withChildren( - saveAndExit, - exit, - status - ) - ).render() - - println(s"Now open ${session.uiUrl} to view the UI") - // wait for one of the save/exit buttons to be pressed. - session.waitTillUserClosesSessionOr(exitFlag.get()) + .onChange: newValue => + csvMap((x, y)) = newValue + status.withText(s"($x,$y) value changed to $newValue").renderChanges() + + Seq( + TableContainer().withChildren( + Table(variant = "striped", colorScheme = Some("teal"), size = "mg") + .withChildren( + TableCaption(text = "Please edit the csv contents above and click save to save and exit"), + Thead(), + Tbody( + children = csv.zipWithIndex.map: (row, y) => + Tr( + children = row.zipWithIndex.map: (column, x) => + Td().withChildren(newEditable(x, y, column)) + ) + ) + ) + ), + HStack().withChildren( + saveAndExit, + exit, + status + ) + ).render() + + println(s"Now open ${session.uiUrl} to view the UI") + // wait for one of the save/exit buttons to be pressed. + session.waitTillUserClosesSessionOr(exitFlag.get()) diff --git a/example-scripts/csv-viewer.sc b/example-scripts/csv-viewer.sc index bf084500..24c5e5ac 100755 --- a/example-scripts/csv-viewer.sc +++ b/example-scripts/csv-viewer.sc @@ -27,24 +27,26 @@ val contents = FileUtils.readFileToString(file, "UTF-8") val csv = contents.split("\n").map(_.split(",")) -Sessions.withNewSession(s"csv-viewer-$fileName", s"CsvView: $fileName"): session => - given ConnectedSession = session - - TableContainer() - .withChildren( - Table(variant = "striped", colorScheme = Some("teal"), size = "mg") - .withChildren( - TableCaption(text = "Csv file contents"), - Tbody( - children = csv.map: row => - Tr( - children = row.map: column => - Td(text = column) - ) +Sessions + .withNewSession(s"csv-viewer-$fileName", s"CsvView: $fileName") + .connect: session => + given ConnectedSession = session + + TableContainer() + .withChildren( + Table(variant = "striped", colorScheme = Some("teal"), size = "mg") + .withChildren( + TableCaption(text = "Csv file contents"), + Tbody( + children = csv.map: row => + Tr( + children = row.map: column => + Td(text = column) + ) + ) ) - ) - ) - .render() - println(s"Now open ${session.uiUrl} to view the UI.") - // since this is a read-only UI, we can exit the app but leave the session open on the UI for the user to examine the data. - session.leaveSessionOpenAfterExiting() + ) + .render() + println(s"Now open ${session.uiUrl} to view the UI.") + // since this is a read-only UI, we can exit the app but leave the session open on the UI for the user to examine the data. + session.leaveSessionOpenAfterExiting() diff --git a/example-scripts/hello-world.sc b/example-scripts/hello-world.sc index 6057391d..43dd1d1d 100755 --- a/example-scripts/hello-world.sc +++ b/example-scripts/hello-world.sc @@ -9,8 +9,10 @@ import org.terminal21.client.components.* // std components, https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdElement.scala import org.terminal21.client.components.std.* -Sessions.withNewSession("hello-world", "Hello World Example"): session => - given ConnectedSession = session +Sessions + .withNewSession("hello-world", "Hello World Example") + .connect: session => + given ConnectedSession = session - Paragraph(text = "Hello World!").render() - session.leaveSessionOpenAfterExiting() + Paragraph(text = "Hello World!").render() + session.leaveSessionOpenAfterExiting() diff --git a/example-scripts/mathjax.sc b/example-scripts/mathjax.sc index 30c2ea72..5a4a0cee 100755 --- a/example-scripts/mathjax.sc +++ b/example-scripts/mathjax.sc @@ -4,14 +4,17 @@ import org.terminal21.client.* import org.terminal21.client.components.* import org.terminal21.client.components.mathjax.* -Sessions.withNewSession("mathjax", "MathJax Example", MathJaxLib /* note we need to register the MathJaxLib in order to use it */ ): session => - given ConnectedSession = session - Seq( - MathJax( - expression = """When \(a \ne 0\), there are two solutions to \(ax^2 + bx + c = 0\) and they are $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$""" - ), - MathJax( - expression = """ +Sessions + .withNewSession("mathjax", "MathJax Example") + .andLibraries(MathJaxLib /* note we need to register the MathJaxLib in order to use it */ ) + .connect: session => + given ConnectedSession = session + Seq( + MathJax( + expression = """When \(a \ne 0\), there are two solutions to \(ax^2 + bx + c = 0\) and they are $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$""" + ), + MathJax( + expression = """ |when \(a \ne 0\), there are two solutions to \(x = {-b \pm \sqrt{b^2-4ac} \over 2a}.\) |Aenean vel velit a lacus lacinia pulvinar. Morbi eget ex et tellus aliquam molestie sit amet eu diam. |Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas tellus enim, tempor non efficitur et, rutrum efficitur metus. @@ -21,6 +24,6 @@ Sessions.withNewSession("mathjax", "MathJax Example", MathJaxLib /* note we need |Morbi ultrices sem quis nisl convallis, ac cursus nunc condimentum. Orci varius natoque penatibus et magnis dis parturient montes, |nascetur ridiculus mus. |""".stripMargin - ) - ).render() - session.leaveSessionOpenAfterExiting() + ) + ).render() + session.leaveSessionOpenAfterExiting() diff --git a/example-scripts/nivo-bar-chart.sc b/example-scripts/nivo-bar-chart.sc index 546d1205..2a15732e 100755 --- a/example-scripts/nivo-bar-chart.sc +++ b/example-scripts/nivo-bar-chart.sc @@ -9,47 +9,50 @@ import org.terminal21.client.components.nivo.* import scala.util.Random import NivoBarChart.* -Sessions.withNewSession("nivo-bar-chart", "Nivo Bar Chart", NivoLib /* note we need to register the NivoLib in order to use it */ ): session => - given ConnectedSession = session +Sessions + .withNewSession("nivo-bar-chart", "Nivo Bar Chart") + .andLibraries(NivoLib /* note we need to register the NivoLib in order to use it */ ) + .connect: session => + given ConnectedSession = session - val chart = ResponsiveBar( - data = createRandomData, - keys = Seq("hot dog", "burger", "sandwich", "kebab", "fries", "donut"), - indexBy = "country", - padding = 0.3, - defs = Seq( - Defs("dots", "patternDots", "inherit", "#38bcb2", size = Some(4), padding = Some(1), stagger = Some(true)), - Defs("lines", "patternLines", "inherit", "#eed312", rotation = Some(-45), lineWidth = Some(6), spacing = Some(10)) - ), - fill = Seq(Fill("dots", Match("fries")), Fill("lines", Match("sandwich"))), - axisLeft = Some(Axis(legend = "food", legendOffset = -40)), - axisBottom = Some(Axis(legend = "country", legendOffset = 32)), - valueScale = Scale(`type` = "linear"), - indexScale = Scale(`type` = "band", round = Some(true)), - legends = Seq( - Legend( - dataFrom = "keys", - translateX = 120, - itemsSpacing = 2, - itemWidth = 100, - itemHeight = 20, - symbolSize = 20, - symbolShape = "square" + val chart = ResponsiveBar( + data = createRandomData, + keys = Seq("hot dog", "burger", "sandwich", "kebab", "fries", "donut"), + indexBy = "country", + padding = 0.3, + defs = Seq( + Defs("dots", "patternDots", "inherit", "#38bcb2", size = Some(4), padding = Some(1), stagger = Some(true)), + Defs("lines", "patternLines", "inherit", "#eed312", rotation = Some(-45), lineWidth = Some(6), spacing = Some(10)) + ), + fill = Seq(Fill("dots", Match("fries")), Fill("lines", Match("sandwich"))), + axisLeft = Some(Axis(legend = "food", legendOffset = -40)), + axisBottom = Some(Axis(legend = "country", legendOffset = 32)), + valueScale = Scale(`type` = "linear"), + indexScale = Scale(`type` = "band", round = Some(true)), + legends = Seq( + Legend( + dataFrom = "keys", + translateX = 120, + itemsSpacing = 2, + itemWidth = 100, + itemHeight = 20, + symbolSize = 20, + symbolShape = "square" + ) ) ) - ) - Seq( - Paragraph(text = "Various foods.", style = Map("margin" -> 20)), - chart - ).render() + Seq( + Paragraph(text = "Various foods.", style = Map("margin" -> 20)), + chart + ).render() - fiberExecutor.submit: - while !session.isClosed do - Thread.sleep(2000) - chart.withData(createRandomData).renderChanges() + fiberExecutor.submit: + while !session.isClosed do + Thread.sleep(2000) + chart.withData(createRandomData).renderChanges() - session.waitTillUserClosesSession() + session.waitTillUserClosesSession() object NivoBarChart: def createRandomData: Seq[Seq[BarDatum]] = diff --git a/example-scripts/nivo-line-chart.sc b/example-scripts/nivo-line-chart.sc index 6f5843ad..e44be1b9 100755 --- a/example-scripts/nivo-line-chart.sc +++ b/example-scripts/nivo-line-chart.sc @@ -9,31 +9,34 @@ import org.terminal21.client.components.nivo.* import scala.util.Random import NivoLineChart.* -Sessions.withNewSession("nivo-line-chart", "Nivo Line Chart", NivoLib /* note we need to register the NivoLib in order to use it */ ): session => - given ConnectedSession = session - - val chart = ResponsiveLine( - data = createRandomData, - yScale = Scale(stacked = Some(true)), - axisBottom = Some(Axis(legend = "transportation", legendOffset = 36)), - axisLeft = Some(Axis(legend = "count", legendOffset = -40)), - legends = Seq(Legend()) - ) +Sessions + .withNewSession("nivo-line-chart", "Nivo Line Chart") + .andLibraries(NivoLib /* note we need to register the NivoLib in order to use it */ ) + .connect: session => + given ConnectedSession = session + + val chart = ResponsiveLine( + data = createRandomData, + yScale = Scale(stacked = Some(true)), + axisBottom = Some(Axis(legend = "transportation", legendOffset = 36)), + axisLeft = Some(Axis(legend = "count", legendOffset = -40)), + legends = Seq(Legend()) + ) - Seq( - Paragraph(text = "Means of transportation for various countries", style = Map("margin" -> 20)), - chart - ).render() + Seq( + Paragraph(text = "Means of transportation for various countries", style = Map("margin" -> 20)), + chart + ).render() - fiberExecutor.submit: - while !session.isClosed do - Thread.sleep(2000) - chart.withData(createRandomData).renderChanges() + fiberExecutor.submit: + while !session.isClosed do + Thread.sleep(2000) + chart.withData(createRandomData).renderChanges() - session.waitTillUserClosesSession() + session.waitTillUserClosesSession() object NivoLineChart: - def createRandomData: Seq[Serie] = + def createRandomData: Seq[Serie] = Seq( dataFor("Japan"), dataFor("France"), diff --git a/example-scripts/on-change.sc b/example-scripts/on-change.sc index a55b92e9..7650f5c8 100755 --- a/example-scripts/on-change.sc +++ b/example-scripts/on-change.sc @@ -5,23 +5,25 @@ import org.terminal21.client.components.* import org.terminal21.client.components.std.Paragraph import org.terminal21.client.components.chakra.* -Sessions.withNewSession("on-change-example", "On Change event handler"): session => - given ConnectedSession = session +Sessions + .withNewSession("on-change-example", "On Change event handler") + .connect: session => + given ConnectedSession = session - val output = Paragraph(text = "Please modify the email.") - val email = Input(`type` = "email", value = "my@email.com").onChange: v => - output.withText(s"Email value : $v").renderChanges() + val output = Paragraph(text = "Please modify the email.") + val email = Input(`type` = "email", value = "my@email.com").onChange: v => + output.withText(s"Email value : $v").renderChanges() - Seq( - FormControl().withChildren( - FormLabel(text = "Email address"), - InputGroup().withChildren( - InputLeftAddon().withChildren(EmailIcon()), - email + Seq( + FormControl().withChildren( + FormLabel(text = "Email address"), + InputGroup().withChildren( + InputLeftAddon().withChildren(EmailIcon()), + email + ), + FormHelperText(text = "We'll never share your email.") ), - FormHelperText(text = "We'll never share your email.") - ), - output - ).render() + output + ).render() - session.waitTillUserClosesSession() + session.waitTillUserClosesSession() diff --git a/example-scripts/on-click.sc b/example-scripts/on-click.sc index ef877a14..462bbc8a 100755 --- a/example-scripts/on-click.sc +++ b/example-scripts/on-click.sc @@ -5,15 +5,17 @@ import org.terminal21.client.components.* import org.terminal21.client.components.std.* import org.terminal21.client.components.chakra.* -Sessions.withNewSession("on-click-example", "On Click Handler"): session => - given ConnectedSession = session +Sessions + .withNewSession("on-click-example", "On Click Handler") + .connect: session => + given ConnectedSession = session - @volatile var exit = false - val msg = Paragraph(text = "Waiting for user to click the button") - val button = Button(text = "Please click me").onClick: () => - msg.withText("Button clicked.").renderChanges() - exit = true + @volatile var exit = false + val msg = Paragraph(text = "Waiting for user to click the button") + val button = Button(text = "Please click me").onClick: () => + msg.withText("Button clicked.").renderChanges() + exit = true - Seq(msg, button).render() + Seq(msg, button).render() - session.waitTillUserClosesSessionOr(exit) + session.waitTillUserClosesSessionOr(exit) diff --git a/example-scripts/postit.sc b/example-scripts/postit.sc index 4c2326a7..e84ffa37 100755 --- a/example-scripts/postit.sc +++ b/example-scripts/postit.sc @@ -12,36 +12,38 @@ import org.terminal21.client.components.std.* // The scala case classes : https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala import org.terminal21.client.components.chakra.* -Sessions.withNewSession("postit", "Post-It"): session => - given ConnectedSession = session +Sessions + .withNewSession("postit", "Post-It") + .connect: session => + given ConnectedSession = session - val editor = Textarea(placeholder = "Please post your note by clicking here and editing the content") - val messages = VStack(align = Some("stretch")) - val add = Button(text = "Post It.").onClick: () => - // add the new msg. - // note: editor.value is automatically updated by terminal-ui - val currentMessages = messages.current - currentMessages - .addChildren( - HStack().withChildren( - Image( - src = "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fa/Apple_Notes_icon.svg/2048px-Apple_Notes_icon.svg.png", - boxSize = Some("32px") - ), - Box(text = editor.current.value) + val editor = Textarea(placeholder = "Please post your note by clicking here and editing the content") + val messages = VStack(align = Some("stretch")) + val add = Button(text = "Post It.").onClick: () => + // add the new msg. + // note: editor.value is automatically updated by terminal-ui + val currentMessages = messages.current + currentMessages + .addChildren( + HStack().withChildren( + Image( + src = "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fa/Apple_Notes_icon.svg/2048px-Apple_Notes_icon.svg.png", + boxSize = Some("32px") + ), + Box(text = editor.current.value) + ) ) - ) - .renderChanges() + .renderChanges() - Seq( - Paragraph(text = "Please type your note below and click 'Post It' to post it so that everyone can view it."), - InputGroup().withChildren( - InputLeftAddon().withChildren(EditIcon()), - editor - ), - add, - messages - ).render() + Seq( + Paragraph(text = "Please type your note below and click 'Post It' to post it so that everyone can view it."), + InputGroup().withChildren( + InputLeftAddon().withChildren(EditIcon()), + editor + ), + add, + messages + ).render() - println(s"Now open ${session.uiUrl} to view the UI.") - session.waitTillUserClosesSession() + println(s"Now open ${session.uiUrl} to view the UI.") + session.waitTillUserClosesSession() diff --git a/example-scripts/progress.sc b/example-scripts/progress.sc index 29782daa..78389aaa 100755 --- a/example-scripts/progress.sc +++ b/example-scripts/progress.sc @@ -5,26 +5,28 @@ import org.terminal21.client.components.* import org.terminal21.client.components.std.* import org.terminal21.client.components.chakra.* -Sessions.withNewSession("universe-generation", "Universe Generation Progress"): session => - given ConnectedSession = session +Sessions + .withNewSession("universe-generation", "Universe Generation Progress") + .connect: session => + given ConnectedSession = session - val msg = Paragraph(text = "Generating universe ...") - val progress = Progress(value = 1) + val msg = Paragraph(text = "Generating universe ...") + val progress = Progress(value = 1) - Seq(msg, progress).render() + Seq(msg, progress).render() - for i <- 1 to 100 do - val p = progress.withValue(i) - val m = - if i < 10 then msg - else if i < 30 then msg.withText("Creating atoms") - else if i < 50 then msg.withText("Big bang!") - else if i < 80 then msg.withText("Inflating") - else msg.withText("Life evolution") + for i <- 1 to 100 do + val p = progress.withValue(i) + val m = + if i < 10 then msg + else if i < 30 then msg.withText("Creating atoms") + else if i < 50 then msg.withText("Big bang!") + else if i < 80 then msg.withText("Inflating") + else msg.withText("Life evolution") - Seq(p, m).renderChanges() - Thread.sleep(100) + Seq(p, m).renderChanges() + Thread.sleep(100) - // clear UI - session.clear() - Paragraph(text = "Universe ready!").render() + // clear UI + session.clear() + Paragraph(text = "Universe ready!").render() diff --git a/example-scripts/project.scala b/example-scripts/project.scala index a8914413..37dfdb86 100644 --- a/example-scripts/project.scala +++ b/example-scripts/project.scala @@ -1,8 +1,8 @@ //> using jvm "21" //> using scala 3 -//> using dep io.github.kostaskougios::terminal21-ui-std:0.21 -//> using dep io.github.kostaskougios::terminal21-nivo:0.21 -//> using dep io.github.kostaskougios::terminal21-mathjax:0.21 +//> using dep io.github.kostaskougios::terminal21-ui-std:0.30 +//> using dep io.github.kostaskougios::terminal21-nivo:0.30 +//> using dep io.github.kostaskougios::terminal21-mathjax:0.30 //> using dep commons-io:commons-io:2.15.1 diff --git a/example-scripts/read-changed-value.sc b/example-scripts/read-changed-value.sc index 65f2693d..4e899edc 100755 --- a/example-scripts/read-changed-value.sc +++ b/example-scripts/read-changed-value.sc @@ -5,26 +5,28 @@ import org.terminal21.client.components.* import org.terminal21.client.components.std.Paragraph import org.terminal21.client.components.chakra.* -Sessions.withNewSession("read-changed-value-example", "Read Changed Value"): session => - given ConnectedSession = session +Sessions + .withNewSession("read-changed-value-example", "Read Changed Value") + .connect: session => + given ConnectedSession = session - val email = Input(`type` = "email", value = "my@email.com") - val output = Box() + val email = Input(`type` = "email", value = "my@email.com") + val output = Box() - Seq( - FormControl().withChildren( - FormLabel(text = "Email address"), - InputGroup().withChildren( - InputLeftAddon().withChildren(EmailIcon()), - email + Seq( + FormControl().withChildren( + FormLabel(text = "Email address"), + InputGroup().withChildren( + InputLeftAddon().withChildren(EmailIcon()), + email + ), + FormHelperText(text = "We'll never share your email.") ), - FormHelperText(text = "We'll never share your email.") - ), - Button(text = "Read Value").onClick: () => - val value = email.current.value - output.current.addChildren(Paragraph(text = s"The value now is $value")).renderChanges() - , - output - ).render() + Button(text = "Read Value").onClick: () => + val value = email.current.value + output.current.addChildren(Paragraph(text = s"The value now is $value")).renderChanges() + , + output + ).render() - session.waitTillUserClosesSession() + session.waitTillUserClosesSession() diff --git a/example-scripts/server.sc b/example-scripts/server.sc index 08612e3d..297a6440 100755 --- a/example-scripts/server.sc +++ b/example-scripts/server.sc @@ -2,7 +2,7 @@ //> using jvm "21" //> using scala 3 -//> using dep io.github.kostaskougios::terminal21-server:0.21 +//> using dep io.github.kostaskougios::terminal21-server-app:0.30 import org.terminal21.server.Terminal21Server diff --git a/example-scripts/textedit.sc b/example-scripts/textedit.sc index 77fe5756..b6c4a787 100755 --- a/example-scripts/textedit.sc +++ b/example-scripts/textedit.sc @@ -31,56 +31,58 @@ val contents = def saveFile(content: String) = FileUtils.writeStringToFile(file, content, "UTF-8") -Sessions.withNewSession(s"textedit-$fileName", s"Edit: $fileName"): session => - given ConnectedSession = session - // we will wait till the user clicks the "Exit" menu, this latch makes sure the main thread of the app waits. - val exitLatch = new CountDownLatch(1) - // the main editor area. - val editor = Textarea(value = contents) - // This will display a "saved" badge for a second when the user saves the file - val status = Badge() - // This will display an asterisk when the contents of the file are changed in the editor - val modified = Badge(colorScheme = Some("red")) +Sessions + .withNewSession(s"textedit-$fileName", s"Edit: $fileName") + .connect: session => + given ConnectedSession = session + // we will wait till the user clicks the "Exit" menu, this latch makes sure the main thread of the app waits. + val exitLatch = new CountDownLatch(1) + // the main editor area. + val editor = Textarea(value = contents) + // This will display a "saved" badge for a second when the user saves the file + val status = Badge() + // This will display an asterisk when the contents of the file are changed in the editor + val modified = Badge(colorScheme = Some("red")) - // when the user changes the textarea, we get the new text and we can compare it with the loaded value. - editor.onChange: newValue => - modified.withText(if newValue != contents then "*" else "").renderChanges() + // when the user changes the textarea, we get the new text and we can compare it with the loaded value. + editor.onChange: newValue => + modified.withText(if newValue != contents then "*" else "").renderChanges() - Seq( - HStack().withChildren( - Menu().withChildren( - MenuButton(text = "File").withChildren(ChevronDownIcon()), - MenuList().withChildren( - MenuItem(text = "Save") - .onClick: () => - saveFile(editor.current.value) - // we'll display a "Saved" badge for 1 second. - Seq( - status.withText("Saved"), - modified.withText("") - ).renderChanges() - // each event handler runs on a new fiber, it is ok to sleep here - Thread.sleep(1000) - status.withText("").renderChanges() - , - MenuItem(text = "Exit") - .onClick: () => - exitLatch.countDown() - ) + Seq( + HStack().withChildren( + Menu().withChildren( + MenuButton(text = "File").withChildren(ChevronDownIcon()), + MenuList().withChildren( + MenuItem(text = "Save") + .onClick: () => + saveFile(editor.current.value) + // we'll display a "Saved" badge for 1 second. + Seq( + status.withText("Saved"), + modified.withText("") + ).renderChanges() + // each event handler runs on a new fiber, it is ok to sleep here + Thread.sleep(1000) + status.withText("").renderChanges() + , + MenuItem(text = "Exit") + .onClick: () => + exitLatch.countDown() + ) + ), + status, + modified ), - status, - modified - ), - FormControl().withChildren( - FormLabel(text = "Editor"), - InputGroup().withChildren( - InputLeftAddon().withChildren(EditIcon()), - editor + FormControl().withChildren( + FormLabel(text = "Editor"), + InputGroup().withChildren( + InputLeftAddon().withChildren(EditIcon()), + editor + ) ) - ) - ).render() + ).render() - println(s"Now open ${session.uiUrl} to view the UI") - exitLatch.await() - session.clear() - Paragraph(text = "Terminated").render() + println(s"Now open ${session.uiUrl} to view the UI") + exitLatch.await() + session.clear() + Paragraph(text = "Terminated").render() diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 3ff78f77..77cf5329 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -77,7 +77,6 @@ class ServerStatusAppInternal(serverSideSessions: ServerSideSessions, sessionsSe .onClick: () => serverSideSessions .withNewSession(session.id + "-server-state", s"Server State:${session.id}") - .andOptions(SessionOptions.CloseTabWhenTerminated) .connect: sSession => new ViewServerState(sSession).runFor(sessionsService.sessionStateOf(session)) ) diff --git a/terminal21-server-client-common/src/main/scala/org/terminal21/model/SessionOptions.scala b/terminal21-server-client-common/src/main/scala/org/terminal21/model/SessionOptions.scala index eb7a8821..fdba9954 100644 --- a/terminal21-server-client-common/src/main/scala/org/terminal21/model/SessionOptions.scala +++ b/terminal21-server-client-common/src/main/scala/org/terminal21/model/SessionOptions.scala @@ -1,7 +1,7 @@ package org.terminal21.model -case class SessionOptions(closeTabWhenTerminated: Boolean = false, alwaysOpen: Boolean = false) +case class SessionOptions(closeTabWhenTerminated: Boolean = true, alwaysOpen: Boolean = false) object SessionOptions: - val Defaults = SessionOptions() - val CloseTabWhenTerminated = SessionOptions(closeTabWhenTerminated = true) + val Defaults = SessionOptions() + val LeaveOpenWhenTerminated = SessionOptions(closeTabWhenTerminated = false) diff --git a/terminal21-server/src/main/scala/org/terminal21/server/service/ServerSessionsService.scala b/terminal21-server/src/main/scala/org/terminal21/server/service/ServerSessionsService.scala index 5aed7c89..18c101a3 100644 --- a/terminal21-server/src/main/scala/org/terminal21/server/service/ServerSessionsService.scala +++ b/terminal21-server/src/main/scala/org/terminal21/server/service/ServerSessionsService.scala @@ -31,6 +31,7 @@ class ServerSessionsService extends SessionsService: override def terminateSession(session: Session): Unit = val state = sessions.getOrElse(session, throw new IllegalArgumentException(s"Session ${session.id} doesn't exist")) + if session.options.alwaysOpen then throw new IllegalArgumentException("Can't terminate a session that should be always open") state.eventsNotificationRegistry.notifyAll(SessionClosed("-")) sessions -= session sessions += session.close -> state.close diff --git a/terminal21-server/src/test/scala/org/terminal21/server/service/ServerSessionsServiceTest.scala b/terminal21-server/src/test/scala/org/terminal21/server/service/ServerSessionsServiceTest.scala index f2bae7e5..fff56a71 100644 --- a/terminal21-server/src/test/scala/org/terminal21/server/service/ServerSessionsServiceTest.scala +++ b/terminal21-server/src/test/scala/org/terminal21/server/service/ServerSessionsServiceTest.scala @@ -45,11 +45,18 @@ class ServerSessionsServiceTest extends AnyFunSuiteLike: test("terminateSession marks session as closed"): new App: - val session = createSession() + val session = createSession(SessionOptions.LeaveOpenWhenTerminated) serverSessionsService.setSessionJsonState(session, serverJson()) serverSessionsService.terminateSession(session) serverSessionsService.sessionById(session.id).isOpen should be(false) + test("terminateSession doesn't terminate a session that should always be open"): + new App: + val session = createSession(SessionOptions(alwaysOpen = true)) + serverSessionsService.setSessionJsonState(session, serverJson()) + an[IllegalArgumentException] should be thrownBy: + serverSessionsService.terminateSession(session) + test("terminateSession removes session if marked to be deleted when terminated"): new App: val session = createSession(SessionOptions(closeTabWhenTerminated = true)) @@ -78,10 +85,11 @@ class ServerSessionsServiceTest extends AnyFunSuiteLike: listenerCalled match case 0 => sessions should be(Seq(session)) case 1 => sessions should be(Seq(session.close)) + case 2 => sessions should be(Nil) listenerCalled += 1 true serverSessionsService.terminateSession(session) - listenerCalled should be(2) + listenerCalled should be(3) test("createSession notifies listeners"): new App: From f302c04ba42bcd43d5edea045fc773e0030f77b8 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 15 Feb 2024 14:15:06 +0000 Subject: [PATCH 037/313] - --- .../scala/tests/StateSessionStateBug.scala | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala diff --git a/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala b/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala new file mode 100644 index 00000000..063b7f8f --- /dev/null +++ b/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala @@ -0,0 +1,45 @@ +package tests + +import org.terminal21.client.components.* +import org.terminal21.client.components.chakra.* +import org.terminal21.client.components.std.Paragraph +import org.terminal21.client.{ConnectedSession, Sessions} + +import java.util.Date + +@main def stateSessionStateBug(): Unit = + Sessions + .withNewSession("state-session", "Stale Session") + .connect: session => + given ConnectedSession = session + + var exitFlag = false + val date = new Date() + Seq( + Paragraph(text = s"Now: $date"), + QuickTable() + .headers("Title", "Value") + .rows( + Seq( + Seq( + "Date - Editable", + Editable(defaultValue = date.toString) + .withChildren( + EditablePreview(), + EditableInput() + ) + ), + Seq( + "Date - Input", + Input(value = date.toString) + ), + Seq( + "Date - Std Input", + std.Input(defaultValue = Some(date.toString)) + ) + ) + ), + Button(text = "Close").onClick: () => + exitFlag = true + ).render() + session.waitTillUserClosesSessionOr(exitFlag) From 5e71723cd5e566179d18a167181abd1b081d4887 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 15 Feb 2024 14:44:01 +0000 Subject: [PATCH 038/313] - --- example-scripts/on-click.sc | 2 ++ example-scripts/progress.sc | 2 ++ 2 files changed, 4 insertions(+) diff --git a/example-scripts/on-click.sc b/example-scripts/on-click.sc index 462bbc8a..69c6a57c 100755 --- a/example-scripts/on-click.sc +++ b/example-scripts/on-click.sc @@ -4,9 +4,11 @@ import org.terminal21.client.* import org.terminal21.client.components.* import org.terminal21.client.components.std.* import org.terminal21.client.components.chakra.* +import org.terminal21.model.SessionOptions Sessions .withNewSession("on-click-example", "On Click Handler") + .andOptions(SessionOptions.LeaveOpenWhenTerminated) .connect: session => given ConnectedSession = session diff --git a/example-scripts/progress.sc b/example-scripts/progress.sc index 78389aaa..1bd6456d 100755 --- a/example-scripts/progress.sc +++ b/example-scripts/progress.sc @@ -4,9 +4,11 @@ import org.terminal21.client.* import org.terminal21.client.components.* import org.terminal21.client.components.std.* import org.terminal21.client.components.chakra.* +import org.terminal21.model.SessionOptions Sessions .withNewSession("universe-generation", "Universe Generation Progress") + .andOptions(SessionOptions.LeaveOpenWhenTerminated) /* leave the session tab open after terminating */ .connect: session => given ConnectedSession = session From e08dbe515a444321e49a87fbcdd68b3d6d887370 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 15 Feb 2024 15:11:33 +0000 Subject: [PATCH 039/313] - --- example-spark/project.scala | 8 +- example-spark/spark-notebook.sc | 137 ++++++++++++++++++-------------- 2 files changed, 81 insertions(+), 64 deletions(-) diff --git a/example-spark/project.scala b/example-spark/project.scala index 72a41970..c4788bc7 100644 --- a/example-spark/project.scala +++ b/example-spark/project.scala @@ -8,10 +8,10 @@ //> using javaOpt -Dlogback.configurationFile=file:etc/logback.xml // terminal21 dependencies -//> using dep io.github.kostaskougios::terminal21-ui-std:0.21 -//> using dep io.github.kostaskougios::terminal21-spark:0.21 -//> using dep io.github.kostaskougios::terminal21-nivo:0.21 -//> using dep io.github.kostaskougios::terminal21-mathjax:0.21 +//> using dep io.github.kostaskougios::terminal21-ui-std:0.30 +//> using dep io.github.kostaskougios::terminal21-spark:0.30 +//> using dep io.github.kostaskougios::terminal21-nivo:0.30 +//> using dep io.github.kostaskougios::terminal21-mathjax:0.30 //> using dep ch.qos.logback:logback-classic:1.4.14 diff --git a/example-spark/spark-notebook.sc b/example-spark/spark-notebook.sc index 02a6a39c..e6383138 100755 --- a/example-spark/spark-notebook.sc +++ b/example-spark/spark-notebook.sc @@ -1,12 +1,11 @@ #!/usr/bin/env -S scala-cli --restart project.scala -/** - * note we use the --restart param for scala-cli. This means every time we change this file, scala-cli will terminate - * and rerun it with the changes. This way we get the notebook feel when we use spark scripts. - * - * terminal21 spark lib caches datasets by storing them into disk. This way complex queries won't have to be re-evaluated - * on each restart of the script. We can force re-evaluation by clicking the "Recalculate" buttons in the UI. - */ +/** note we use the --restart param for scala-cli. This means every time we change this file, scala-cli will terminate and rerun it with the changes. This way + * we get the notebook feel when we use spark scripts. + * + * terminal21 spark lib caches datasets by storing them into disk. This way complex queries won't have to be re-evaluated on each restart of the script. We can + * force re-evaluation by clicking the "Recalculate" buttons in the UI. + */ // We need these imports import org.apache.spark.sql.* @@ -17,70 +16,88 @@ import org.terminal21.client.{*, given} import org.terminal21.sparklib.* import java.util.concurrent.atomic.AtomicInteger -import scala.util.Random +import scala.util.{Random, Using} import SparkNotebook.* import org.terminal21.client.components.mathjax.{MathJax, MathJaxLib} +import org.terminal21.client.components.std.Paragraph -SparkSessions.newTerminal21WithSparkSession(SparkSessions.newSparkSession(/* configure your spark session here */), "spark-notebook", "Spark Notebook", NivoLib, MathJaxLib): (spark, session) => - given ConnectedSession = session - given SparkSession = spark - import scala3encoders.given - import spark.implicits.* +Using.resource(SparkSessions.newSparkSession( /* configure your spark session here */ )): spark => + Sessions + .withNewSession("spark-notebook", "Spark Notebook") + .andLibraries(NivoLib, MathJaxLib) + .connect: session => + given ConnectedSession = session + given SparkSession = spark + import scala3encoders.given + import spark.implicits.* - // lets get a Dataset, the data are random so that when we click refresh we can see the data actually - // been refreshed. - val peopleDS = createPeople + // lets get a Dataset, the data are random so that when we click refresh we can see the data actually + // been refreshed. + val peopleDS = createPeople - // We will display the data in a table - val peopleTable = QuickTable().headers("Id", "Name", "Age").caption("People") + // We will display the data in a table + val peopleTable = QuickTable().headers("Id", "Name", "Age").caption("People") - val peopleTableCalc = peopleDS.sort($"id").visualize("People sample", peopleTable): data => - peopleTable.rows(data.take(5).map(p => Seq(p.id, p.name, p.age))) + val peopleTableCalc = peopleDS + .sort($"id") + .visualize("People sample", peopleTable): data => + peopleTable.rows(data.take(5).map(p => Seq(p.id, p.name, p.age))) - /** The calculation above uses a directory to store the dataset results. This way we can restart this script without loosing datasets that may take long to - * calculate, making our script behave more like a notebook. When we click "Recalculate" in the UI, the cache directory is deleted and the dataset is - * re-evaluated. If the Dataset schema changes, please click "Recalculate" or manually delete this folder. - * - * The key for the cache is "People sample" - */ - println(s"Cache path: ${peopleTableCalc.cachePath}") + /** The calculation above uses a directory to store the dataset results. This way we can restart this script without loosing datasets that may take long + * to calculate, making our script behave more like a notebook. When we click "Recalculate" in the UI, the cache directory is deleted and the dataset is + * re-evaluated. If the Dataset schema changes, please click "Recalculate" or manually delete this folder. + * + * The key for the cache is "People sample" + */ + println(s"Cache path: ${peopleTableCalc.cachePath}") - val oldestPeopleChart = ResponsiveLine( - axisBottom = Some(Axis(legend = "Person", legendOffset = 36)), - axisLeft = Some(Axis(legend = "Age", legendOffset = -40)), - legends = Seq(Legend()) - ) + val oldestPeopleChart = ResponsiveLine( + axisBottom = Some(Axis(legend = "Person", legendOffset = 36)), + axisLeft = Some(Axis(legend = "Age", legendOffset = -40)), + legends = Seq(Legend()) + ) - val oldestPeopleChartCalc = peopleDS - .orderBy($"age".desc) - .visualize("Oldest people", oldestPeopleChart): data => - oldestPeopleChart.withData(Seq( - Serie( - "Person", - data = data.take(5).map(person => Datum(person.name, person.age)) - ) - )) + val oldestPeopleChartCalc = peopleDS + .orderBy($"age".desc) + .visualize("Oldest people", oldestPeopleChart): data => + oldestPeopleChart.withData( + Seq( + Serie( + "Person", + data = data.take(5).map(person => Datum(person.name, person.age)) + ) + ) + ) - Seq( - // just make it look a bit more like a proper notebook by adding some fake maths - MathJax( - expression = """ - |The following is total nonsense but it simulates some explanation that would normally be here if we had - |a proper notebook. When \(a \ne 0\), there are two solutions to \(x = {-b \pm \sqrt{b^2-4ac} \over 2a}.\) - |Aenean vel velit a lacus lacinia pulvinar. Morbi eget ex et tellus aliquam molestie sit amet eu diam. - |Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas tellus enim, tempor non efficitur et, rutrum efficitur metus. - |Nulla scelerisque, mauris sit amet accumsan iaculis, elit ipsum suscipit lorem, sed fermentum nunc purus non tellus. - |Aenean congue accumsan tempor. \(x = {-b \pm \sqrt{b^2-4ac} \over 2a}.\) maecenas vitae commodo tortor. Aliquam erat volutpat. Etiam laoreet malesuada elit sed vestibulum. - |Etiam consequat congue fermentum. Vivamus dapibus scelerisque ipsum eu tempus. Integer non pulvinar nisi. - |Morbi ultrices sem quis nisl convallis, ac cursus nunc condimentum. Orci varius natoque penatibus et magnis dis parturient montes, - |nascetur ridiculus mus. - |""".stripMargin - ), - peopleTableCalc, - oldestPeopleChartCalc - ).render() + Seq( + Paragraph( + text = """ + |The spark notebooks can use the `visualise` extension method over a dataframe/dataset. It will cache the dataset by + |saving it as a file under /tmp. The `Recalculate` button refreshes the dataset (re-runs it). In this example, the + |data are random and so are different each time the `Recalculate` is pressed. + |""".stripMargin, + style = Map("margin" -> "32px") + ), + // just make it look a bit more like a proper notebook by adding some fake maths + MathJax( + expression = """ + |The following is total nonsense but it simulates some explanation that would normally be here if we had + |a proper notebook. When \(a \ne 0\), there are two solutions to \(x = {-b \pm \sqrt{b^2-4ac} \over 2a}.\) + |Aenean vel velit a lacus lacinia pulvinar. Morbi eget ex et tellus aliquam molestie sit amet eu diam. + |Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas tellus enim, tempor non efficitur et, rutrum efficitur metus. + |Nulla scelerisque, mauris sit amet accumsan iaculis, elit ipsum suscipit lorem, sed fermentum nunc purus non tellus. + |Aenean congue accumsan tempor. \(x = {-b \pm \sqrt{b^2-4ac} \over 2a}.\) maecenas vitae commodo tortor. Aliquam erat volutpat. Etiam laoreet malesuada elit sed vestibulum. + |Etiam consequat congue fermentum. Vivamus dapibus scelerisque ipsum eu tempus. Integer non pulvinar nisi. + |Morbi ultrices sem quis nisl convallis, ac cursus nunc condimentum. Orci varius natoque penatibus et magnis dis parturient montes, + |nascetur ridiculus mus. + |""".stripMargin, + style = Map("margin" -> "32px") + ), + peopleTableCalc, + oldestPeopleChartCalc + ).render() - session.waitTillUserClosesSession() + session.waitTillUserClosesSession() object SparkNotebook: private val names = Array("Andy", "Kostas", "Alex", "Andreas", "George", "Jack") From 43b22b00ffa66fadc1ad3118c963380d8f516ea0 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 15 Feb 2024 15:42:14 +0000 Subject: [PATCH 040/313] - --- Readme.md | 3 +- docs/quick.md | 18 ++ docs/tutorial.md | 187 ++++++++++-------- example-scripts/server.sc | 1 + .../client/components/chakra/QuickTable.scala | 11 +- 5 files changed, 133 insertions(+), 87 deletions(-) diff --git a/Readme.md b/Readme.md index b8881889..c312f471 100644 --- a/Readme.md +++ b/Readme.md @@ -99,7 +99,7 @@ session it has with the server and terminate the app. # Usecases -Due to it's client-server architecture, terminal21 gives a UI to scripts running i.e. on servers without a desktop environment and +Due to its client-server architecture, terminal21 gives a UI to scripts running i.e. on servers without a desktop environment and can be used for things like: - creating text file editors which run on desktop-less servers but still allows us to edit the text file on our browser, see [textedit.sc](example-scripts/textedit.sc) @@ -170,6 +170,7 @@ Please use the [discussions](https://github.com/kostaskougios/terminal21-restapi - apps can now run on the server + server management bundled apps - session builders refactoring for more flexible creation of sessions - QuickTabs +- bug fix for old react state re-rendering on new session ## Version 0.21 diff --git a/docs/quick.md b/docs/quick.md index 3f1558d1..616e0709 100644 --- a/docs/quick.md +++ b/docs/quick.md @@ -5,6 +5,8 @@ simplify creation of this components. ## QuickTable +This class helps creating tables quickly. + ```scala val conversionTable = QuickTable().headers("To convert", "into", "multiply by") .caption("Imperial to metric conversion factors") @@ -14,3 +16,19 @@ val tableRows:Seq[Seq[String]] = Seq( ) conversionTable.rows(tableRows) ``` + +## QuickTabs + +This class simplifies the creation of tabs. + +```scala + +QuickTabs() + .withTabs("Tab 1", "Tab 2") + .withTabPanels( + Paragraph(text="Tab 1 content"), + Paragraph(text="Tab 2 content") + ) + +``` + diff --git a/docs/tutorial.md b/docs/tutorial.md index e81d3cea..a6742422 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -28,6 +28,7 @@ The easiest way to start the terminal21 server is to have a `scala-cli` script o //> using jvm "21" //> using scala 3 +//> using javaOpt -Xmx128m //> using dep io.github.kostaskougios::terminal21-server:_VERSION_ import org.terminal21.server.Terminal21Server @@ -61,15 +62,19 @@ To do this we can create a [hello-world.sc](../example-scripts/hello-world.sc) i ```scala #!/usr/bin/env -S scala-cli project.scala +// always import these import org.terminal21.client.* import org.terminal21.client.components.* +// std components, https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdElement.scala import org.terminal21.client.components.std.* -Sessions.withNewSession("hello-world", "Hello World Example"): session => - given ConnectedSession = session +Sessions + .withNewSession("hello-world", "Hello World Example") + .connect: session => + given ConnectedSession = session - Paragraph(text = "Hello World!").render() - session.leaveSessionOpenAfterExiting() + Paragraph(text = "Hello World!").render() + session.leaveSessionOpenAfterExiting() ``` The first line, `#!/usr/bin/env -S scala-cli project.scala`, makes our script runnable from the command line. @@ -89,8 +94,10 @@ Next it creates a session. Each session has a unique id (globally unique across title, "Hello World Example", that will be displayed on the browser. ```scala -Sessions.withNewSession("hello-world", "Hello World Example"): session => - ... +Sessions + .withNewSession("hello-world", "Hello World Example") + .connect: session => + ... ``` ![hello-world](images/hello-world.png) @@ -129,30 +136,34 @@ import org.terminal21.client.* import org.terminal21.client.components.* import org.terminal21.client.components.std.* import org.terminal21.client.components.chakra.* - -Sessions.withNewSession("universe-generation", "Universe Generation Progress"): session => - given ConnectedSession = session - - val msg = Paragraph(text = "Generating universe ...") - val progress = Progress(value = 1) - - Seq(msg, progress).render() - - for i <- 1 to 100 do - val p = progress.withValue(i) - val m = - if i < 10 then msg - else if i < 30 then msg.withText("Creating atoms") - else if i < 50 then msg.withText("Big bang!") - else if i < 80 then msg.withText("Inflating") - else msg.withText("Life evolution") - - Seq(p, m).renderChanges() - Thread.sleep(100) - - // clear UI - session.clear() - Paragraph(text = "Universe ready!").render() +import org.terminal21.model.SessionOptions + +Sessions + .withNewSession("universe-generation", "Universe Generation Progress") + .andOptions(SessionOptions.LeaveOpenWhenTerminated) /* leave the session tab open after terminating */ + .connect: session => + given ConnectedSession = session + + val msg = Paragraph(text = "Generating universe ...") + val progress = Progress(value = 1) + + Seq(msg, progress).render() + + for i <- 1 to 100 do + val p = progress.withValue(i) + val m = + if i < 10 then msg + else if i < 30 then msg.withText("Creating atoms") + else if i < 50 then msg.withText("Big bang!") + else if i < 80 then msg.withText("Inflating") + else msg.withText("Life evolution") + + Seq(p, m).renderChanges() + Thread.sleep(100) + + // clear UI + session.clear() + Paragraph(text = "Universe ready!").render() ``` Here we create a paragraph and a progress bar. @@ -202,19 +213,23 @@ import org.terminal21.client.* import org.terminal21.client.components.* import org.terminal21.client.components.std.* import org.terminal21.client.components.chakra.* - -Sessions.withNewSession("on-click-example", "On Click Handler"): session => - given ConnectedSession = session - - @volatile var exit = false - val msg = Paragraph(text = "Waiting for user to click the button") - val button = Button(text = "Please click me").onClick: () => - msg.withText("Button clicked.").renderChanges() - exit = true - - Seq(msg, button).render() - - session.waitTillUserClosesSessionOr(exit) +import org.terminal21.model.SessionOptions + +Sessions + .withNewSession("on-click-example", "On Click Handler") + .andOptions(SessionOptions.LeaveOpenWhenTerminated) + .connect: session => + given ConnectedSession = session + + @volatile var exit = false + val msg = Paragraph(text = "Waiting for user to click the button") + val button = Button(text = "Please click me").onClick: () => + msg.withText("Button clicked.").renderChanges() + exit = true + + Seq(msg, button).render() + + session.waitTillUserClosesSessionOr(exit) ``` First we create the paragraph and button. We attach an `onClick` handler on the button: @@ -254,29 +269,31 @@ import org.terminal21.client.components.* import org.terminal21.client.components.std.Paragraph import org.terminal21.client.components.chakra.* -Sessions.withNewSession("read-changed-value-example", "Read Changed Value"): session => - given ConnectedSession = session - - val email = Input(`type` = "email", value = "my@email.com") - val output = Box() - - Seq( - FormControl().withChildren( - FormLabel(text = "Email address"), - InputGroup().withChildren( - InputLeftAddon().withChildren(EmailIcon()), - email +Sessions + .withNewSession("read-changed-value-example", "Read Changed Value") + .connect: session => + given ConnectedSession = session + + val email = Input(`type` = "email", value = "my@email.com") + val output = Box() + + Seq( + FormControl().withChildren( + FormLabel(text = "Email address"), + InputGroup().withChildren( + InputLeftAddon().withChildren(EmailIcon()), + email + ), + FormHelperText(text = "We'll never share your email.") ), - FormHelperText(text = "We'll never share your email.") - ), - Button(text = "Read Value").onClick: () => - val value = email.current.value - output.current.addChildren(Paragraph(text = s"The value now is $value")).renderChanges() - , - output - ).render() - - session.waitTillUserClosesSession() + Button(text = "Read Value").onClick: () => + val value = email.current.value + output.current.addChildren(Paragraph(text = s"The value now is $value")).renderChanges() + , + output + ).render() + + session.waitTillUserClosesSession() ``` The important bit is this: @@ -306,26 +323,28 @@ import org.terminal21.client.components.* import org.terminal21.client.components.std.Paragraph import org.terminal21.client.components.chakra.* -Sessions.withNewSession("on-change-example", "On Change event handler"): session => - given ConnectedSession = session - - val output = Paragraph(text = "Please modify the email.") - val email = Input(`type` = "email", value = "my@email.com").onChange: v => - output.withText(s"Email value : $v").renderChanges() - - Seq( - FormControl().withChildren( - FormLabel(text = "Email address"), - InputGroup().withChildren( - InputLeftAddon().withChildren(EmailIcon()), - email +Sessions + .withNewSession("on-change-example", "On Change event handler") + .connect: session => + given ConnectedSession = session + + val output = Paragraph(text = "Please modify the email.") + val email = Input(`type` = "email", value = "my@email.com").onChange: v => + output.withText(s"Email value : $v").renderChanges() + + Seq( + FormControl().withChildren( + FormLabel(text = "Email address"), + InputGroup().withChildren( + InputLeftAddon().withChildren(EmailIcon()), + email + ), + FormHelperText(text = "We'll never share your email.") ), - FormHelperText(text = "We'll never share your email.") - ), - output - ).render() - - session.waitTillUserClosesSession() + output + ).render() + + session.waitTillUserClosesSession() ``` The important bit are these lines: diff --git a/example-scripts/server.sc b/example-scripts/server.sc index 297a6440..7ba686b7 100755 --- a/example-scripts/server.sc +++ b/example-scripts/server.sc @@ -2,6 +2,7 @@ //> using jvm "21" //> using scala 3 +//> using javaOpt -Xmx128m //> using dep io.github.kostaskougios::terminal21-server-app:0.30 import org.terminal21.server.Terminal21Server diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala index 8ba731be..8f80b566 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala @@ -40,8 +40,15 @@ case class QuickTable( val tableContainer = TableContainer(key = key + "-tc", style = style, children = Seq(table)) Seq(tableContainer) - def headers(headers: String*): QuickTable = copy(headers = headers.map(h => Text(text = h))) - def headersElements(headers: UiElement*): QuickTable = copy(headers = headers) + def headers(headers: String*): QuickTable = copy(headers = headers.map(h => Text(text = h))) + def headersElements(headers: UiElement*): QuickTable = copy(headers = headers) + + /** @param data + * A mix of plain types or UiElement. If it is a UiElement, it will be rendered otherwise if it is anything else the `.toString` method will be used to + * render it. + * @return + * QuickTable + */ def rows(data: Seq[Seq[Any]]): QuickTable = copy(rows = data.map(_.map: case u: UiElement => u case c => Text(text = c.toString) From daeffd52086007d3bde86b283486c31f2d97c931 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 15 Feb 2024 16:58:56 +0000 Subject: [PATCH 041/313] - --- .../src/main/scala/tests/ChakraComponents.scala | 2 +- .../org/terminal21/client/components/std/StdElement.scala | 8 +++----- .../org/terminal21/client/components/std/StdHttp.scala | 8 +++----- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala index 74447147..c5578cd9 100644 --- a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala @@ -23,7 +23,7 @@ import java.util.concurrent.atomic.AtomicBoolean val latch = new CountDownLatch(1) // react tests reset the session to clear state - val krButton = Button(text = "Keep Running").onClick: () => + val krButton = Button(text = "Reset state").onClick: () => keepRunning.set(true) latch.countDown() diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala index 8ee72beb..810388cb 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala @@ -1,5 +1,6 @@ package org.terminal21.client.components.std +import org.terminal21.client.OnChangeEventHandler.CanHandleOnChangeEvent import org.terminal21.client.components.UiElement.{Current, HasChildren, HasEventHandler, HasStyle} import org.terminal21.client.components.{Keys, UiElement} import org.terminal21.client.{ConnectedSession, OnChangeEventHandler} @@ -70,14 +71,11 @@ case class Input( style: Map[String, Any] = Map.empty, value: Option[String] = None ) extends StdElement[Input] - with HasEventHandler: + with HasEventHandler + with CanHandleOnChangeEvent[Input]: override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = newValue => session.modified(copy(value = Some(newValue))) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withType(v: String) = copy(`type` = v) def withDefaultValue(v: Option[String]) = copy(defaultValue = v) def withValue(v: Option[String]) = copy(value = v) - - def onChange(h: OnChangeEventHandler)(using session: ConnectedSession): Input = - session.addEventHandler(key, h) - this diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala index 03376e86..71762997 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala @@ -1,5 +1,6 @@ package org.terminal21.client.components.std +import org.terminal21.client.OnChangeEventHandler.CanHandleOnChangeEvent import org.terminal21.client.{ConnectedSession, EventHandler, OnChangeEventHandler} import org.terminal21.client.components.UiElement.HasEventHandler import org.terminal21.client.components.{Keys, TransientRequest, UiElement} @@ -40,9 +41,6 @@ case class CookieReader( value: Option[String] = None, // will be set when/if cookie value is read requestId: String = TransientRequest.newRequestId() ) extends StdHttp - with HasEventHandler: + with HasEventHandler + with CanHandleOnChangeEvent[CookieReader]: override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = newValue => session.modified(copy(value = Some(newValue))) - - def onChange(h: OnChangeEventHandler)(using session: ConnectedSession): CookieReader = - session.addEventHandler(key, h) - this From 319b57608676c65fa12b44f478ae1e105ec42606 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 15 Feb 2024 17:04:40 +0000 Subject: [PATCH 042/313] - --- Readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Readme.md b/Readme.md index c312f471..6f086768 100644 --- a/Readme.md +++ b/Readme.md @@ -168,6 +168,7 @@ Please use the [discussions](https://github.com/kostaskougios/terminal21-restapi ## Version 0.30 - apps can now run on the server + server management bundled apps +- Cookie setter and reader. - session builders refactoring for more flexible creation of sessions - QuickTabs - bug fix for old react state re-rendering on new session From 633650cc98594444f321797f16d42b70f8cf30fb Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 15 Feb 2024 19:18:34 +0000 Subject: [PATCH 043/313] - --- .../src/main/scala/tests/StdComponents.scala | 2 +- .../terminal21/client/ConnectedSession.scala | 18 ++++++++++++++++++ .../org/terminal21/client/EventHandler.scala | 4 ++++ .../client/ConnectedSessionTest.scala | 12 +++++++++++- 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/StdComponents.scala b/end-to-end-tests/src/main/scala/tests/StdComponents.scala index ee8192b4..b3bfa006 100644 --- a/end-to-end-tests/src/main/scala/tests/StdComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/StdComponents.scala @@ -30,7 +30,7 @@ import org.terminal21.client.components.std.* NewLine(), Span(text = "And the last line") ), - Paragraph(text = "A Form ").withChildren( + Paragraph(text = "A Form").withChildren( input ), output, diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index a6d81dee..914d92dc 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -19,10 +19,14 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se private val handlers = new EventHandlers(this) def uiUrl: String = serverUrl + "/ui" + + /** Clears all UI elements and event handlers. Renders a blank UI + */ def clear(): Unit = render() handlers.clear() modifiedElements.clear() + removeGlobalEventHandler() def addEventHandler(key: String, handler: EventHandler): Unit = handlers.addEventHandler(key, handler) @@ -58,6 +62,18 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se def click(e: UiElement): Unit = fireEvent(OnClick(e.key)) + @volatile private var globalEventHandler: Option[GlobalEventHandler] = None + + /** Registers a global event handler who will handle all received events. + * + * @param h + * GlobalEventHandler + */ + def withGlobalEventHandler(h: GlobalEventHandler): Unit = + globalEventHandler = Some(h) + + def removeGlobalEventHandler(): Unit = globalEventHandler = None + def fireEvent(event: CommandEvent): Unit = try event match @@ -75,6 +91,8 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se case x => logger.error(s"Unknown event handling combination : $x") case None => logger.warn(s"There is no event handler for event $event") + + for h <- globalEventHandler do h.onEvent(event) catch case t: Throwable => logger.error(s"Session ${session.id}: An error occurred while handling $event", t) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala index 1705b951..1af8fac5 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala @@ -1,6 +1,7 @@ package org.terminal21.client import org.terminal21.client.components.UiElement +import org.terminal21.model.CommandEvent trait EventHandler @@ -33,3 +34,6 @@ object OnChangeBooleanEventHandler: def onChange(h: OnChangeBooleanEventHandler)(using session: ConnectedSession): A = session.addEventHandler(key, h) this + +trait GlobalEventHandler extends EventHandler: + def onEvent(event: CommandEvent): Unit diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala index 5b26595c..abedb169 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala @@ -7,11 +7,21 @@ import org.scalatest.matchers.should.Matchers.* import org.terminal21.client.ConnectedSessionMock.encoder import org.terminal21.client.components.chakra.Editable import org.terminal21.client.components.std.{Paragraph, Span} -import org.terminal21.model.OnChange +import org.terminal21.model.{CommandEvent, OnChange} import org.terminal21.ui.std.ServerJson class ConnectedSessionTest extends AnyFunSuiteLike: + test("global event handler is called on event"): + given connectedSession: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock + val editable = Editable() + var received = Option.empty[CommandEvent] + connectedSession.withGlobalEventHandler: event => + received = Some(event) + val event = OnChange(editable.key, "new value") + connectedSession.fireEvent(event) + received should be(Some(event)) + test("default event handlers are invoked before user handlers"): given connectedSession: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock val editable = Editable() From 5851af6065db0c44453d809df4cc604935bd82d9 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 15 Feb 2024 19:20:44 +0000 Subject: [PATCH 044/313] - --- .../scala/org/terminal21/client/ConnectedSession.scala | 2 +- .../org/terminal21/client/components/UiElement.scala | 4 ++++ .../org/terminal21/client/ConnectedSessionTest.scala | 9 +++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index 914d92dc..1341104f 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -103,7 +103,7 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se sessionsService.setSessionJsonState(session, j) def renderChanges(es: UiElement*): Unit = - for e <- es do modified(e) + for e <- es.flatMap(_.flat) do modified(e) val j = toJson(es) sessionsService.changeSessionJsonState(session, j) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala index 00bb95b0..a5ac84fd 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala @@ -4,6 +4,10 @@ import org.terminal21.client.{ConnectedSession, EventHandler} trait UiElement: def key: String + + /** @return + * this element along all it's children flattened + */ def flat: Seq[UiElement] = Seq(this) def render()(using session: ConnectedSession): Unit = diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala index abedb169..67cdcf20 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala @@ -71,3 +71,12 @@ class ConnectedSessionTest extends AnyFunSuiteLike: connectedSession.render(p1) connectedSession.renderChanges(p1.withChildren(span1)) p1.current.children should be(Seq(span1)) + + test("renderChanges updates current version of component when component deeply nested"): + given connectedSession: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock + + val span1 = Span(text = "span1") + val p1 = Paragraph(text = "p1").withChildren(span1) + connectedSession.render(p1) + connectedSession.renderChanges(p1.withChildren(span1.withText("span-text-changed"))) + span1.current.text should be("span-text-changed") From 8f77dbcf4502edcab299e5fafd636fb016e5fc9a Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 15 Feb 2024 19:27:17 +0000 Subject: [PATCH 045/313] - --- .../scala/org/terminal21/client/ConnectedSession.scala | 8 ++++++-- .../main/scala/org/terminal21/client/EventHandler.scala | 2 +- .../org/terminal21/client/ConnectedSessionTest.scala | 4 +++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index 1341104f..340f2f43 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -72,6 +72,8 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se def withGlobalEventHandler(h: GlobalEventHandler): Unit = globalEventHandler = Some(h) + /** removes the global event handler (if any). No more events will be received by that handler. + */ def removeGlobalEventHandler(): Unit = globalEventHandler = None def fireEvent(event: CommandEvent): Unit = @@ -92,12 +94,14 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se case None => logger.warn(s"There is no event handler for event $event") - for h <- globalEventHandler do h.onEvent(event) + for h <- globalEventHandler do h.onEvent(event, modifiedElements(event.key)) catch case t: Throwable => logger.error(s"Session ${session.id}: An error occurred while handling $event", t) throw t - def render(es: UiElement*): Unit = + + def render(es: UiElement*): Unit = + for e <- es.flatMap(_.flat) do modified(e) handlers.registerEventHandlers(es) val j = toJson(es) sessionsService.setSessionJsonState(session, j) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala index 1af8fac5..dc0721d2 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala @@ -36,4 +36,4 @@ object OnChangeBooleanEventHandler: this trait GlobalEventHandler extends EventHandler: - def onEvent(event: CommandEvent): Unit + def onEvent(event: CommandEvent, receivedBy: UiElement): Unit diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala index 67cdcf20..6401debb 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala @@ -15,9 +15,11 @@ class ConnectedSessionTest extends AnyFunSuiteLike: test("global event handler is called on event"): given connectedSession: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock val editable = Editable() + editable.render() var received = Option.empty[CommandEvent] - connectedSession.withGlobalEventHandler: event => + connectedSession.withGlobalEventHandler: (event, e) => received = Some(event) + e should be(editable.withValue("new value")) val event = OnChange(editable.key, "new value") connectedSession.fireEvent(event) received should be(Some(event)) From fbb23e8b31ad0596323c76fae0871bd1004fa675 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 16 Feb 2024 17:54:23 +0000 Subject: [PATCH 046/313] - --- .../org/terminal21/collections/SEList.scala | 48 +++++++++++++++++++ .../terminal21/collections/SEListTest.scala | 36 ++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 terminal21-server-client-common/src/main/scala/org/terminal21/collections/SEList.scala create mode 100644 terminal21-server-client-common/src/test/scala/org/terminal21/collections/SEListTest.scala diff --git a/terminal21-server-client-common/src/main/scala/org/terminal21/collections/SEList.scala b/terminal21-server-client-common/src/main/scala/org/terminal21/collections/SEList.scala new file mode 100644 index 00000000..356635cf --- /dev/null +++ b/terminal21-server-client-common/src/main/scala/org/terminal21/collections/SEList.scala @@ -0,0 +1,48 @@ +package org.terminal21.collections + +import java.util.concurrent.CountDownLatch + +class SEList[A]: + @volatile private var currentNode: NormalNode[A] = NormalNode(None, EndNode) + def iterator: Iterator[A] = new SEBlockingIterator(currentNode) + + def poisonPill(): Unit = + synchronized: + currentNode.valueAndNext = (None, PoisonPillNode) + currentNode.latch.countDown() + + def add(item: A): Unit = + val cn = synchronized: + val cn = currentNode + if cn.valueAndNext._2 == PoisonPillNode then throw new IllegalStateException("Can't add items when the list has been poisoned.") + val n = NormalNode(None, currentNode.valueAndNext._2) + currentNode.valueAndNext = (Some(item), n) + currentNode = n + cn + cn.latch.countDown() + +class SEBlockingIterator[A](@volatile var currentNode: NormalNode[A]) extends Iterator[A]: + override def hasNext: Boolean = + currentNode.waitValue() + val v = currentNode.valueAndNext._2 + if v == PoisonPillNode then false else true + + override def next(): A = + hasNext + val v = currentNode.value + currentNode = currentNode.next + v + +sealed trait Node[+A] +case object EndNode extends Node[Nothing] +case object PoisonPillNode extends Node[Nothing] + +case class NormalNode[A](@volatile var valueAndNext: (Option[A], Node[A])) extends Node[A]: + val latch = new CountDownLatch(1) + def waitValue(): Unit = latch.await() + + def hasValue: Boolean = valueAndNext._1.nonEmpty + def value: A = valueAndNext._1.get + def next: NormalNode[A] = valueAndNext._2 match + case nn: NormalNode[A] @unchecked => nn + case _ => throw new NoSuchElementException("next should be called only if hasValue is true") diff --git a/terminal21-server-client-common/src/test/scala/org/terminal21/collections/SEListTest.scala b/terminal21-server-client-common/src/test/scala/org/terminal21/collections/SEListTest.scala new file mode 100644 index 00000000..512bf4bd --- /dev/null +++ b/terminal21-server-client-common/src/test/scala/org/terminal21/collections/SEListTest.scala @@ -0,0 +1,36 @@ +package org.terminal21.collections + +import functions.fibers.FiberExecutor +import org.scalatest.funsuite.AnyFunSuiteLike +import org.scalatest.matchers.should.Matchers.* + +class SEListTest extends AnyFunSuiteLike: + val executor = FiberExecutor() + test("when empty, it.hasNext should wait"): + val l = SEList[Int]() + val it = l.iterator + l.poisonPill() + it.toList should be(Nil) + + test("when empty with 2 starters"): + val l = SEList[Int]() + val it1 = l.iterator + val it2 = l.iterator + l.poisonPill() + it1.toList should be(Nil) + it2.toList should be(Nil) + + test("with 1 item"): + val l = SEList[Int]() + val it = l.iterator + l.add(1) + l.poisonPill() + it.toList should be(List(1)) + + test("with 2 items"): + val l = SEList[Int]() + val it = l.iterator + l.add(1) + l.add(2) + l.poisonPill() + it.toList should be(List(1, 2)) From 2b7a24e755bca5a4075cf972178b4148c62359e4 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 16 Feb 2024 17:56:14 +0000 Subject: [PATCH 047/313] - --- .../org/terminal21/collections/SEListTest.scala | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/terminal21-server-client-common/src/test/scala/org/terminal21/collections/SEListTest.scala b/terminal21-server-client-common/src/test/scala/org/terminal21/collections/SEListTest.scala index 512bf4bd..2c3fd45d 100644 --- a/terminal21-server-client-common/src/test/scala/org/terminal21/collections/SEListTest.scala +++ b/terminal21-server-client-common/src/test/scala/org/terminal21/collections/SEListTest.scala @@ -12,7 +12,7 @@ class SEListTest extends AnyFunSuiteLike: l.poisonPill() it.toList should be(Nil) - test("when empty with 2 starters"): + test("when empty with 2 iterators"): val l = SEList[Int]() val it1 = l.iterator val it2 = l.iterator @@ -34,3 +34,13 @@ class SEListTest extends AnyFunSuiteLike: l.add(2) l.poisonPill() it.toList should be(List(1, 2)) + + test("with 2 items and 2 iterators"): + val l = SEList[Int]() + val it1 = l.iterator + val it2 = l.iterator + l.add(1) + l.add(2) + l.poisonPill() + it1.toList should be(List(1, 2)) + it2.toList should be(List(1, 2)) From 2d44b0b357e2d525d00abfa8fbcc57932d36aabe Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 16 Feb 2024 18:04:42 +0000 Subject: [PATCH 048/313] - --- .../org/terminal21/collections/SEListTest.scala | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/terminal21-server-client-common/src/test/scala/org/terminal21/collections/SEListTest.scala b/terminal21-server-client-common/src/test/scala/org/terminal21/collections/SEListTest.scala index 2c3fd45d..3dc52538 100644 --- a/terminal21-server-client-common/src/test/scala/org/terminal21/collections/SEListTest.scala +++ b/terminal21-server-client-common/src/test/scala/org/terminal21/collections/SEListTest.scala @@ -44,3 +44,19 @@ class SEListTest extends AnyFunSuiteLike: l.poisonPill() it1.toList should be(List(1, 2)) it2.toList should be(List(1, 2)) + + test("multiple iterators and multi threading"): + val l = SEList[Int]() + val iterators = for _ <- 1 to 100 yield + val it = l.iterator + executor.submit: + it.toList + + for i <- 1 to 100 do + Thread.sleep(1) + l.add(i) + + l.poisonPill() + + val expected = (1 to 100).toList + for f <- iterators do f.get() should be(expected) From 0b3b45e8f62532820fc18cda09bcf5a3245bf587 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 16 Feb 2024 18:06:56 +0000 Subject: [PATCH 049/313] - --- .../scala/org/terminal21/collections/SEListTest.scala | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/terminal21-server-client-common/src/test/scala/org/terminal21/collections/SEListTest.scala b/terminal21-server-client-common/src/test/scala/org/terminal21/collections/SEListTest.scala index 3dc52538..f4fd5c93 100644 --- a/terminal21-server-client-common/src/test/scala/org/terminal21/collections/SEListTest.scala +++ b/terminal21-server-client-common/src/test/scala/org/terminal21/collections/SEListTest.scala @@ -45,9 +45,17 @@ class SEListTest extends AnyFunSuiteLike: it1.toList should be(List(1, 2)) it2.toList should be(List(1, 2)) + test("iterator after added items"): + val l = SEList[Int]() + l.add(1) + val it = l.iterator + l.add(2) + l.poisonPill() + it.toList should be(List(2)) + test("multiple iterators and multi threading"): val l = SEList[Int]() - val iterators = for _ <- 1 to 100 yield + val iterators = for _ <- 1 to 1000 yield val it = l.iterator executor.submit: it.toList From 7b7ca10c578d450ded2f971d22f9268efe5f87c7 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 16 Feb 2024 18:20:48 +0000 Subject: [PATCH 050/313] - --- .../scala/org/terminal21/collections/SEList.scala | 9 +++++---- .../org/terminal21/collections/SEListTest.scala | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/terminal21-server-client-common/src/main/scala/org/terminal21/collections/SEList.scala b/terminal21-server-client-common/src/main/scala/org/terminal21/collections/SEList.scala index 356635cf..a9397e5b 100644 --- a/terminal21-server-client-common/src/main/scala/org/terminal21/collections/SEList.scala +++ b/terminal21-server-client-common/src/main/scala/org/terminal21/collections/SEList.scala @@ -28,10 +28,11 @@ class SEBlockingIterator[A](@volatile var currentNode: NormalNode[A]) extends It if v == PoisonPillNode then false else true override def next(): A = - hasNext - val v = currentNode.value - currentNode = currentNode.next - v + if hasNext then + val v = currentNode.value + currentNode = currentNode.next + v + else throw new NoSuchElementException("next() called but there is no next element. The SEList has been poisoned and we reached the PoisonPill") sealed trait Node[+A] case object EndNode extends Node[Nothing] diff --git a/terminal21-server-client-common/src/test/scala/org/terminal21/collections/SEListTest.scala b/terminal21-server-client-common/src/test/scala/org/terminal21/collections/SEListTest.scala index f4fd5c93..a37c6f94 100644 --- a/terminal21-server-client-common/src/test/scala/org/terminal21/collections/SEListTest.scala +++ b/terminal21-server-client-common/src/test/scala/org/terminal21/collections/SEListTest.scala @@ -6,6 +6,7 @@ import org.scalatest.matchers.should.Matchers.* class SEListTest extends AnyFunSuiteLike: val executor = FiberExecutor() + test("when empty, it.hasNext should wait"): val l = SEList[Int]() val it = l.iterator @@ -53,6 +54,19 @@ class SEListTest extends AnyFunSuiteLike: l.poisonPill() it.toList should be(List(2)) + test("hasNext & next()"): + val l = SEList[Int]() + val it = l.iterator + l.add(1) + l.add(2) + l.poisonPill() + it.hasNext should be(true) + it.next() should be(1) + it.hasNext should be(true) + it.next() should be(2) + it.hasNext should be(false) + an[NoSuchElementException] should be thrownBy (it.next()) + test("multiple iterators and multi threading"): val l = SEList[Int]() val iterators = for _ <- 1 to 1000 yield From 7572c1a99a446403b93b906723235a26c5526012 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 16 Feb 2024 18:32:22 +0000 Subject: [PATCH 051/313] - --- .../terminal21/client/ConnectedSession.scala | 17 +++++++++---- .../org/terminal21/client/EventHandler.scala | 3 ++- .../terminal21/client/model/GlobalEvent.scala | 6 +++++ .../client/ConnectedSessionTest.scala | 24 ++++++++++++++++--- 4 files changed, 42 insertions(+), 8 deletions(-) create mode 100644 terminal21-ui-std/src/main/scala/org/terminal21/client/model/GlobalEvent.scala diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index 340f2f43..cbf73866 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -3,9 +3,11 @@ package org.terminal21.client import io.circe.* import io.circe.generic.auto.* import org.slf4j.LoggerFactory -import org.terminal21.client.components.UiElement.{HasChildren, HasEventHandler, allDeep} +import org.terminal21.client.components.UiElement.HasChildren import org.terminal21.client.components.{UiComponent, UiElement, UiElementEncoding} import org.terminal21.client.internal.EventHandlers +import org.terminal21.client.model.GlobalEvent +import org.terminal21.collections.SEList import org.terminal21.model.* import org.terminal21.ui.std.{ServerJson, SessionsService} @@ -15,8 +17,9 @@ import scala.annotation.tailrec import scala.collection.concurrent.TrieMap class ConnectedSession(val session: Session, encoding: UiElementEncoding, val serverUrl: String, sessionsService: SessionsService, onCloseHandler: () => Unit): - private val logger = LoggerFactory.getLogger(getClass) - private val handlers = new EventHandlers(this) + private val logger = LoggerFactory.getLogger(getClass) + private val handlers = new EventHandlers(this) + @volatile private var events = SEList[GlobalEvent]() def uiUrl: String = serverUrl + "/ui" @@ -27,6 +30,8 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se handlers.clear() modifiedElements.clear() removeGlobalEventHandler() + events.poisonPill() + events = SEList() def addEventHandler(key: String, handler: EventHandler): Unit = handlers.addEventHandler(key, handler) @@ -72,6 +77,8 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se def withGlobalEventHandler(h: GlobalEventHandler): Unit = globalEventHandler = Some(h) + def globalEventIterator: Iterator[GlobalEvent] = events.iterator + /** removes the global event handler (if any). No more events will be received by that handler. */ def removeGlobalEventHandler(): Unit = globalEventHandler = None @@ -94,7 +101,9 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se case None => logger.warn(s"There is no event handler for event $event") - for h <- globalEventHandler do h.onEvent(event, modifiedElements(event.key)) + val globalEvent = GlobalEvent(event, modifiedElements(event.key)) + for h <- globalEventHandler do h.onEvent(globalEvent) + events.add(globalEvent) catch case t: Throwable => logger.error(s"Session ${session.id}: An error occurred while handling $event", t) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala index dc0721d2..2b0dbeb9 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala @@ -1,6 +1,7 @@ package org.terminal21.client import org.terminal21.client.components.UiElement +import org.terminal21.client.model.GlobalEvent import org.terminal21.model.CommandEvent trait EventHandler @@ -36,4 +37,4 @@ object OnChangeBooleanEventHandler: this trait GlobalEventHandler extends EventHandler: - def onEvent(event: CommandEvent, receivedBy: UiElement): Unit + def onEvent(event: GlobalEvent): Unit diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/model/GlobalEvent.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/model/GlobalEvent.scala new file mode 100644 index 00000000..ddf44120 --- /dev/null +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/model/GlobalEvent.scala @@ -0,0 +1,6 @@ +package org.terminal21.client.model + +import org.terminal21.client.components.UiElement +import org.terminal21.model.CommandEvent + +case class GlobalEvent(event: CommandEvent, receivedBy: UiElement) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala index 6401debb..999684fd 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala @@ -7,19 +7,37 @@ import org.scalatest.matchers.should.Matchers.* import org.terminal21.client.ConnectedSessionMock.encoder import org.terminal21.client.components.chakra.Editable import org.terminal21.client.components.std.{Paragraph, Span} +import org.terminal21.client.model.GlobalEvent import org.terminal21.model.{CommandEvent, OnChange} import org.terminal21.ui.std.ServerJson class ConnectedSessionTest extends AnyFunSuiteLike: + test("global event iterator"): + given connectedSession: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock + val editable = Editable() + editable.render() + val it = connectedSession.globalEventIterator + val event1 = OnChange(editable.key, "v1") + val event2 = OnChange(editable.key, "v2") + connectedSession.fireEvent(event1) + connectedSession.fireEvent(event2) + connectedSession.clear() + it.toList should be( + List( + GlobalEvent(event1, editable.withValue("v1")), + GlobalEvent(event2, editable.withValue("v2")) + ) + ) + test("global event handler is called on event"): given connectedSession: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock val editable = Editable() editable.render() var received = Option.empty[CommandEvent] - connectedSession.withGlobalEventHandler: (event, e) => - received = Some(event) - e should be(editable.withValue("new value")) + connectedSession.withGlobalEventHandler: ge => + received = Some(ge.event) + ge.receivedBy should be(editable.withValue("new value")) val event = OnChange(editable.key, "new value") connectedSession.fireEvent(event) received should be(Some(event)) From 044e04165b6bf2ca3bc39c0b8937c28994e38aa6 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 16 Feb 2024 20:08:53 +0000 Subject: [PATCH 052/313] - --- .../src/main/scala/tests/LoginForm.scala | 53 +++++++++++++++++++ .../terminal21/client/ConnectedSession.scala | 13 +++-- .../org/terminal21/client/EventHandler.scala | 4 +- .../terminal21/client/model/GlobalEvent.scala | 9 +++- .../client/ConnectedSessionTest.scala | 6 +-- 5 files changed, 74 insertions(+), 11 deletions(-) create mode 100644 end-to-end-tests/src/main/scala/tests/LoginForm.scala diff --git a/end-to-end-tests/src/main/scala/tests/LoginForm.scala b/end-to-end-tests/src/main/scala/tests/LoginForm.scala new file mode 100644 index 00000000..0a094ea4 --- /dev/null +++ b/end-to-end-tests/src/main/scala/tests/LoginForm.scala @@ -0,0 +1,53 @@ +package tests + +import org.terminal21.client.{ConnectedSession, Sessions} +import org.terminal21.client.components.* +import org.terminal21.client.components.chakra.* + +@main def loginForm(): Unit = + Sessions + .withNewSession("std-components", "Std Components") + .connect: session => + given ConnectedSession = session + + val email = Input(`type` = "email", value = "my@email.com") + val description = Textarea(placeholder = "Please enter a few things about you") + val submitButton = Button(text = "Submit") + val password = Input(`type` = "password", value = "mysecret") + Seq( + FormControl().withChildren( + FormLabel(text = "Email address"), + InputGroup().withChildren( + InputLeftAddon().withChildren(EmailIcon()), + email, + InputRightAddon().withChildren(CheckCircleIcon(color = Some("green"))) + ), + FormHelperText(text = "We'll never share your email.") + ), + FormControl().withChildren( + FormLabel(text = "Description"), + InputGroup().withChildren( + InputLeftAddon().withChildren(EditIcon()), + description + ), + FormHelperText(text = "We'll never share your email.") + ), + FormControl().withChildren( + FormLabel(text = "Password"), + InputGroup().withChildren( + InputLeftAddon().withChildren(ViewOffIcon()), + password + ), + FormHelperText(text = "Don't share with anyone") + ), + submitButton + ).render() + session.globalEventIterator + .filter(_.isReceivedBy(submitButton)) + .take(1) + .foreach: ge => + println(s""" + |email = ${email.current.value} + |desc = ${description.current.value} + |pwd = ${password.current.value} + |""".stripMargin) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index cbf73866..69182887 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -6,7 +6,7 @@ import org.slf4j.LoggerFactory import org.terminal21.client.components.UiElement.HasChildren import org.terminal21.client.components.{UiComponent, UiElement, UiElementEncoding} import org.terminal21.client.internal.EventHandlers -import org.terminal21.client.model.GlobalEvent +import org.terminal21.client.model.{GlobalEvent, SessionClosedEvent, UiEvent} import org.terminal21.collections.SEList import org.terminal21.model.* import org.terminal21.ui.std.{ServerJson, SessionsService} @@ -87,9 +87,14 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se try event match case SessionClosed(_) => + events.add(SessionClosedEvent) exitLatch.countDown() onCloseHandler() case _ => + val globalEvent = UiEvent(event, modifiedElements(event.key)) + for h <- globalEventHandler do h.onEvent(globalEvent) + events.add(globalEvent) + handlers.getEventHandler(event.key) match case Some(handlers) => for handler <- handlers do @@ -101,9 +106,6 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se case None => logger.warn(s"There is no event handler for event $event") - val globalEvent = GlobalEvent(event, modifiedElements(event.key)) - for h <- globalEventHandler do h.onEvent(globalEvent) - events.add(globalEvent) catch case t: Throwable => logger.error(s"Session ${session.id}: An error occurred while handling $event", t) @@ -149,4 +151,5 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se private val modifiedElements = TrieMap.empty[String, UiElement] def modified(e: UiElement): Unit = modifiedElements += e.key -> e - def currentState[A <: UiElement](e: A): A = modifiedElements.getOrElse(e.key, e).asInstanceOf[A] + def currentState[A <: UiElement](e: A): A = + modifiedElements.getOrElse(e.key, throw new IllegalStateException(s"Key ${e.key} doesn't exist or was removed")).asInstanceOf[A] diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala index 2b0dbeb9..b2644e89 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala @@ -1,7 +1,7 @@ package org.terminal21.client import org.terminal21.client.components.UiElement -import org.terminal21.client.model.GlobalEvent +import org.terminal21.client.model.{GlobalEvent, UiEvent} import org.terminal21.model.CommandEvent trait EventHandler @@ -37,4 +37,4 @@ object OnChangeBooleanEventHandler: this trait GlobalEventHandler extends EventHandler: - def onEvent(event: GlobalEvent): Unit + def onEvent(event: UiEvent): Unit diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/model/GlobalEvent.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/model/GlobalEvent.scala index ddf44120..d213ce06 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/model/GlobalEvent.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/model/GlobalEvent.scala @@ -3,4 +3,11 @@ package org.terminal21.client.model import org.terminal21.client.components.UiElement import org.terminal21.model.CommandEvent -case class GlobalEvent(event: CommandEvent, receivedBy: UiElement) +sealed trait GlobalEvent: + def isReceivedBy(e: UiElement): Boolean + +case class UiEvent(event: CommandEvent, receivedBy: UiElement) extends GlobalEvent: + override def isReceivedBy(e: UiElement): Boolean = e == receivedBy + +case object SessionClosedEvent extends GlobalEvent: + override def isReceivedBy(e: UiElement): Boolean = false diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala index 999684fd..57d68aae 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala @@ -7,7 +7,7 @@ import org.scalatest.matchers.should.Matchers.* import org.terminal21.client.ConnectedSessionMock.encoder import org.terminal21.client.components.chakra.Editable import org.terminal21.client.components.std.{Paragraph, Span} -import org.terminal21.client.model.GlobalEvent +import org.terminal21.client.model.{GlobalEvent, UiEvent} import org.terminal21.model.{CommandEvent, OnChange} import org.terminal21.ui.std.ServerJson @@ -25,8 +25,8 @@ class ConnectedSessionTest extends AnyFunSuiteLike: connectedSession.clear() it.toList should be( List( - GlobalEvent(event1, editable.withValue("v1")), - GlobalEvent(event2, editable.withValue("v2")) + UiEvent(event1, editable.withValue("v1")), + UiEvent(event2, editable.withValue("v2")) ) ) From 79e85fb31fda08a7c86bcd26363d8b0db825491f Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 16 Feb 2024 20:35:57 +0000 Subject: [PATCH 053/313] - --- .../src/main/scala/tests/LoginForm.scala | 16 +++++++--------- .../org/terminal21/client/ConnectedSession.scala | 1 + .../terminal21/client/model/GlobalEvent.scala | 3 +++ 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/LoginForm.scala b/end-to-end-tests/src/main/scala/tests/LoginForm.scala index 0a094ea4..9daf246e 100644 --- a/end-to-end-tests/src/main/scala/tests/LoginForm.scala +++ b/end-to-end-tests/src/main/scala/tests/LoginForm.scala @@ -42,12 +42,10 @@ import org.terminal21.client.components.chakra.* ), submitButton ).render() - session.globalEventIterator - .filter(_.isReceivedBy(submitButton)) - .take(1) - .foreach: ge => - println(s""" - |email = ${email.current.value} - |desc = ${description.current.value} - |pwd = ${password.current.value} - |""".stripMargin) + val o = session.globalEventIterator + .takeWhile(e => !e.isSessionClose && !e.isReceivedBy(submitButton)) + .map: _ => + (email.current.value, password.current.value) + .toList + .lastOption + println(o) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index 69182887..80d9a9f4 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -88,6 +88,7 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se event match case SessionClosed(_) => events.add(SessionClosedEvent) + events.poisonPill() exitLatch.countDown() onCloseHandler() case _ => diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/model/GlobalEvent.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/model/GlobalEvent.scala index d213ce06..b1bd3ac1 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/model/GlobalEvent.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/model/GlobalEvent.scala @@ -5,9 +5,12 @@ import org.terminal21.model.CommandEvent sealed trait GlobalEvent: def isReceivedBy(e: UiElement): Boolean + def isSessionClose: Boolean case class UiEvent(event: CommandEvent, receivedBy: UiElement) extends GlobalEvent: override def isReceivedBy(e: UiElement): Boolean = e == receivedBy + override def isSessionClose: Boolean = false case object SessionClosedEvent extends GlobalEvent: override def isReceivedBy(e: UiElement): Boolean = false + override def isSessionClose: Boolean = true From 72c8a232180d6790035cfe6d97974360198599e6 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Sat, 17 Feb 2024 00:19:10 +0000 Subject: [PATCH 054/313] - --- .../src/main/scala/tests/LoginForm.scala | 15 ++++++++------- .../org/terminal21/client/ConnectedSession.scala | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/LoginForm.scala b/end-to-end-tests/src/main/scala/tests/LoginForm.scala index 9daf246e..1f0aaef4 100644 --- a/end-to-end-tests/src/main/scala/tests/LoginForm.scala +++ b/end-to-end-tests/src/main/scala/tests/LoginForm.scala @@ -42,10 +42,11 @@ import org.terminal21.client.components.chakra.* ), submitButton ).render() - val o = session.globalEventIterator - .takeWhile(e => !e.isSessionClose && !e.isReceivedBy(submitButton)) - .map: _ => - (email.current.value, password.current.value) - .toList - .lastOption - println(o) + + case class PersonSubmitted(email: String, pwd: String, isSubmitted: Boolean, userClosedSession: Boolean) + val o = session.globalEventIterator + .map: e => + PersonSubmitted(email.current.value, password.current.value, e.isReceivedBy(submitButton), e.isSessionClose) + .dropWhile(p => !p.isSubmitted && !p.userClosedSession) + .nextOption() + println(o.mkString("\n")) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index 80d9a9f4..0c5507a3 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -105,7 +105,7 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se case (onChange: OnChange, h: OnChangeBooleanEventHandler) => h.onChange(onChange.value.toBoolean) case x => logger.error(s"Unknown event handling combination : $x") case None => - logger.warn(s"There is no event handler for event $event") + // nop catch case t: Throwable => From f53d1c0ecb7628cf51a38b060073c64a0ef129e7 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Sat, 17 Feb 2024 00:26:47 +0000 Subject: [PATCH 055/313] - --- .../src/main/scala/tests/LoginForm.scala | 16 ++++------------ .../org/terminal21/client/ConnectedSession.scala | 2 +- .../terminal21/client/model/GlobalEvent.scala | 6 +++--- .../terminal21/client/ConnectedSessionTest.scala | 2 +- 4 files changed, 9 insertions(+), 17 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/LoginForm.scala b/end-to-end-tests/src/main/scala/tests/LoginForm.scala index 1f0aaef4..3bbb1b12 100644 --- a/end-to-end-tests/src/main/scala/tests/LoginForm.scala +++ b/end-to-end-tests/src/main/scala/tests/LoginForm.scala @@ -24,14 +24,6 @@ import org.terminal21.client.components.chakra.* ), FormHelperText(text = "We'll never share your email.") ), - FormControl().withChildren( - FormLabel(text = "Description"), - InputGroup().withChildren( - InputLeftAddon().withChildren(EditIcon()), - description - ), - FormHelperText(text = "We'll never share your email.") - ), FormControl().withChildren( FormLabel(text = "Password"), InputGroup().withChildren( @@ -44,9 +36,9 @@ import org.terminal21.client.components.chakra.* ).render() case class PersonSubmitted(email: String, pwd: String, isSubmitted: Boolean, userClosedSession: Boolean) - val o = session.globalEventIterator + val p = session.eventIterator .map: e => - PersonSubmitted(email.current.value, password.current.value, e.isReceivedBy(submitButton), e.isSessionClose) + PersonSubmitted(email.current.value, password.current.value, e.isTarget(submitButton), e.isSessionClose) .dropWhile(p => !p.isSubmitted && !p.userClosedSession) - .nextOption() - println(o.mkString("\n")) + .next() + println(p) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index 0c5507a3..4317fd4e 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -77,7 +77,7 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se def withGlobalEventHandler(h: GlobalEventHandler): Unit = globalEventHandler = Some(h) - def globalEventIterator: Iterator[GlobalEvent] = events.iterator + def eventIterator: Iterator[GlobalEvent] = events.iterator /** removes the global event handler (if any). No more events will be received by that handler. */ diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/model/GlobalEvent.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/model/GlobalEvent.scala index b1bd3ac1..c32c83b0 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/model/GlobalEvent.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/model/GlobalEvent.scala @@ -4,13 +4,13 @@ import org.terminal21.client.components.UiElement import org.terminal21.model.CommandEvent sealed trait GlobalEvent: - def isReceivedBy(e: UiElement): Boolean + def isTarget(e: UiElement): Boolean def isSessionClose: Boolean case class UiEvent(event: CommandEvent, receivedBy: UiElement) extends GlobalEvent: - override def isReceivedBy(e: UiElement): Boolean = e == receivedBy + override def isTarget(e: UiElement): Boolean = e == receivedBy override def isSessionClose: Boolean = false case object SessionClosedEvent extends GlobalEvent: - override def isReceivedBy(e: UiElement): Boolean = false + override def isTarget(e: UiElement): Boolean = false override def isSessionClose: Boolean = true diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala index 57d68aae..773cf47f 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala @@ -17,7 +17,7 @@ class ConnectedSessionTest extends AnyFunSuiteLike: given connectedSession: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock val editable = Editable() editable.render() - val it = connectedSession.globalEventIterator + val it = connectedSession.eventIterator val event1 = OnChange(editable.key, "v1") val event2 = OnChange(editable.key, "v2") connectedSession.fireEvent(event1) From 0cc6bde8c14cf182029ca4f53a6514bbe1e3e0b4 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Sat, 17 Feb 2024 00:46:49 +0000 Subject: [PATCH 056/313] - --- .../src/main/scala/tests/LoginForm.scala | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/LoginForm.scala b/end-to-end-tests/src/main/scala/tests/LoginForm.scala index 3bbb1b12..78174470 100644 --- a/end-to-end-tests/src/main/scala/tests/LoginForm.scala +++ b/end-to-end-tests/src/main/scala/tests/LoginForm.scala @@ -10,17 +10,20 @@ import org.terminal21.client.components.chakra.* .connect: session => given ConnectedSession = session - val email = Input(`type` = "email", value = "my@email.com") - val description = Textarea(placeholder = "Please enter a few things about you") - val submitButton = Button(text = "Submit") - val password = Input(`type` = "password", value = "mysecret") + val emailInput = Input(`type` = "email", value = "my@email.com") + val description = Textarea(placeholder = "Please enter a few things about you") + val submitButton = Button(text = "Submit") + val password = Input(`type` = "password", value = "mysecret") + val okIcon = CheckCircleIcon(color = Some("green")) + val notOkIcon = WarningTwoIcon(color = Some("red")) + val emailRightAddon = InputRightAddon().withChildren(okIcon) Seq( FormControl().withChildren( FormLabel(text = "Email address"), InputGroup().withChildren( InputLeftAddon().withChildren(EmailIcon()), - email, - InputRightAddon().withChildren(CheckCircleIcon(color = Some("green"))) + emailInput, + emailRightAddon ), FormHelperText(text = "We'll never share your email.") ), @@ -35,10 +38,16 @@ import org.terminal21.client.components.chakra.* submitButton ).render() - case class PersonSubmitted(email: String, pwd: String, isSubmitted: Boolean, userClosedSession: Boolean) + case class PersonSubmitted(email: String, isValidEmail: Boolean, pwd: String, isSubmitted: Boolean, userClosedSession: Boolean) val p = session.eventIterator .map: e => - PersonSubmitted(email.current.value, password.current.value, e.isTarget(submitButton), e.isSessionClose) - .dropWhile(p => !p.isSubmitted && !p.userClosedSession) + println(e) + val email = emailInput.current.value + PersonSubmitted(email, email.contains("@"), password.current.value, e.isTarget(submitButton), e.isSessionClose) + .tapEach: p => + println(p) + val emailAddon = if p.isValidEmail then emailRightAddon.withChildren(okIcon) else emailRightAddon.withChildren(notOkIcon) + emailAddon.renderChanges() + .dropWhile(p => !(p.isSubmitted && p.isValidEmail) && !p.userClosedSession) .next() - println(p) + println("Result:" + p) From c663cbcdec00ec05f32973706a8f7378a1ae9621 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Sat, 17 Feb 2024 01:02:24 +0000 Subject: [PATCH 057/313] - --- end-to-end-tests/src/main/scala/tests/LoginForm.scala | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/LoginForm.scala b/end-to-end-tests/src/main/scala/tests/LoginForm.scala index 78174470..2b949e64 100644 --- a/end-to-end-tests/src/main/scala/tests/LoginForm.scala +++ b/end-to-end-tests/src/main/scala/tests/LoginForm.scala @@ -11,9 +11,8 @@ import org.terminal21.client.components.chakra.* given ConnectedSession = session val emailInput = Input(`type` = "email", value = "my@email.com") - val description = Textarea(placeholder = "Please enter a few things about you") val submitButton = Button(text = "Submit") - val password = Input(`type` = "password", value = "mysecret") + val passwordInput = Input(`type` = "password", value = "mysecret") val okIcon = CheckCircleIcon(color = Some("green")) val notOkIcon = WarningTwoIcon(color = Some("red")) val emailRightAddon = InputRightAddon().withChildren(okIcon) @@ -31,7 +30,7 @@ import org.terminal21.client.components.chakra.* FormLabel(text = "Password"), InputGroup().withChildren( InputLeftAddon().withChildren(ViewOffIcon()), - password + passwordInput ), FormHelperText(text = "Don't share with anyone") ), @@ -41,11 +40,10 @@ import org.terminal21.client.components.chakra.* case class PersonSubmitted(email: String, isValidEmail: Boolean, pwd: String, isSubmitted: Boolean, userClosedSession: Boolean) val p = session.eventIterator .map: e => - println(e) val email = emailInput.current.value - PersonSubmitted(email, email.contains("@"), password.current.value, e.isTarget(submitButton), e.isSessionClose) + val pwd = passwordInput.current.value + PersonSubmitted(email, email.contains("@"), pwd, e.isTarget(submitButton), e.isSessionClose) .tapEach: p => - println(p) val emailAddon = if p.isValidEmail then emailRightAddon.withChildren(okIcon) else emailRightAddon.withChildren(notOkIcon) emailAddon.renderChanges() .dropWhile(p => !(p.isSubmitted && p.isValidEmail) && !p.userClosedSession) From 1aaca92cc2a5719bfcd0dc0a41f3811a52f26c2b Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Sat, 17 Feb 2024 01:25:51 +0000 Subject: [PATCH 058/313] - --- .../src/main/scala/tests/LoginForm.scala | 13 ++++----- .../client/components/UiElement.scala | 6 ----- .../components/chakra/QuickFormControl.scala | 27 +++++++++++++++++++ .../client/internal/EventHandlers.scala | 6 ++--- 4 files changed, 37 insertions(+), 15 deletions(-) create mode 100644 terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala diff --git a/end-to-end-tests/src/main/scala/tests/LoginForm.scala b/end-to-end-tests/src/main/scala/tests/LoginForm.scala index 2b949e64..7be2feb2 100644 --- a/end-to-end-tests/src/main/scala/tests/LoginForm.scala +++ b/end-to-end-tests/src/main/scala/tests/LoginForm.scala @@ -6,7 +6,7 @@ import org.terminal21.client.components.chakra.* @main def loginForm(): Unit = Sessions - .withNewSession("std-components", "Std Components") + .withNewSession("login-form", "Login Form") .connect: session => given ConnectedSession = session @@ -17,15 +17,14 @@ import org.terminal21.client.components.chakra.* val notOkIcon = WarningTwoIcon(color = Some("red")) val emailRightAddon = InputRightAddon().withChildren(okIcon) Seq( - FormControl().withChildren( - FormLabel(text = "Email address"), - InputGroup().withChildren( + QuickFormControl() + .withLabel("Email address") + .withHelperText("We'll never share your email.") + .withInputGroup( InputLeftAddon().withChildren(EmailIcon()), emailInput, emailRightAddon ), - FormHelperText(text = "We'll never share your email.") - ), FormControl().withChildren( FormLabel(text = "Password"), InputGroup().withChildren( @@ -40,10 +39,12 @@ import org.terminal21.client.components.chakra.* case class PersonSubmitted(email: String, isValidEmail: Boolean, pwd: String, isSubmitted: Boolean, userClosedSession: Boolean) val p = session.eventIterator .map: e => + println(e) val email = emailInput.current.value val pwd = passwordInput.current.value PersonSubmitted(email, email.contains("@"), pwd, e.isTarget(submitButton), e.isSessionClose) .tapEach: p => + println(p) val emailAddon = if p.isValidEmail then emailRightAddon.withChildren(okIcon) else emailRightAddon.withChildren(notOkIcon) emailAddon.renderChanges() .dropWhile(p => !(p.isSubmitted && p.isValidEmail) && !p.userClosedSession) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala index a5ac84fd..34eff4d6 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala @@ -19,12 +19,6 @@ trait UiElement: session.renderChanges(this) object UiElement: - def allDeep(elements: Seq[UiElement]): Seq[UiElement] = - elements ++ elements - .collect: - case hc: HasChildren[_] => allDeep(hc.children) - .flatten - trait Current[A <: UiElement]: this: UiElement => def current(using session: ConnectedSession): A = session.currentState(this.asInstanceOf[A]) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala new file mode 100644 index 00000000..cd912a5c --- /dev/null +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala @@ -0,0 +1,27 @@ +package org.terminal21.client.components.chakra + +import org.terminal21.client.components.{Keys, UiComponent, UiElement} +import org.terminal21.client.components.UiElement.HasStyle + +case class QuickFormControl( + key: String = Keys.nextKey, + style: Map[String, Any] = Map.empty, + label: Option[String] = None, + inputGroup: Seq[UiElement] = Nil, + helperText: Option[String] = None +) extends UiComponent + with HasStyle[QuickFormControl]: + lazy val rendered: Seq[UiElement] = + val ch: Seq[UiElement] = + label.map(l => FormLabel(key = key + "-label", text = l)).toSeq ++ + Seq(InputGroup(key = key + "-ig").withChildren(inputGroup: _*)) ++ + helperText.map(h => FormHelperText(key = key + "-helper", text = h)) + Seq( + FormControl(key = key + "-fc", style = style).withChildren(ch: _*) + ) + + def withLabel(label: String): QuickFormControl = copy(label = Some(label)) + def withInputGroup(ig: UiElement*): QuickFormControl = copy(inputGroup = ig) + def withHelperText(text: String): QuickFormControl = copy(helperText = Some(text)) + + override def withStyle(v: Map[String, Any]): QuickFormControl = copy(style = v) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/internal/EventHandlers.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/internal/EventHandlers.scala index 39ba715e..3146b74f 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/internal/EventHandlers.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/internal/EventHandlers.scala @@ -1,14 +1,14 @@ package org.terminal21.client.internal -import org.terminal21.client.{ConnectedSession, EventHandler} import org.terminal21.client.components.UiElement -import org.terminal21.client.components.UiElement.{HasEventHandler, allDeep} +import org.terminal21.client.components.UiElement.HasEventHandler +import org.terminal21.client.{ConnectedSession, EventHandler} class EventHandlers(session: ConnectedSession): private val eventHandlers = collection.concurrent.TrieMap.empty[String, List[EventHandler]] def registerEventHandlers(es: Seq[UiElement]): Unit = synchronized: - val all = allDeep(es) + val all = es.flatMap(_.flat) val withEvents = all.collect: case h: HasEventHandler => h From 089e26d50928a4097f1a56469497faf2afda4386 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Sat, 17 Feb 2024 01:31:38 +0000 Subject: [PATCH 059/313] - --- .../org/terminal21/client/ConnectedSession.scala | 11 ++++------- .../org/terminal21/client/ConnectedSessionTest.scala | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index 4317fd4e..661d8725 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -92,10 +92,6 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se exitLatch.countDown() onCloseHandler() case _ => - val globalEvent = UiEvent(event, modifiedElements(event.key)) - for h <- globalEventHandler do h.onEvent(globalEvent) - events.add(globalEvent) - handlers.getEventHandler(event.key) match case Some(handlers) => for handler <- handlers do @@ -104,9 +100,10 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se case (onChange: OnChange, h: OnChangeEventHandler) => h.onChange(onChange.value) case (onChange: OnChange, h: OnChangeBooleanEventHandler) => h.onChange(onChange.value.toBoolean) case x => logger.error(s"Unknown event handling combination : $x") - case None => - // nop - + case None => // nop + val globalEvent = UiEvent(event, modifiedElements(event.key)) + for h <- globalEventHandler do h.onEvent(globalEvent) + events.add(globalEvent) catch case t: Throwable => logger.error(s"Session ${session.id}: An error occurred while handling $event", t) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala index 773cf47f..cedd6739 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala @@ -7,7 +7,7 @@ import org.scalatest.matchers.should.Matchers.* import org.terminal21.client.ConnectedSessionMock.encoder import org.terminal21.client.components.chakra.Editable import org.terminal21.client.components.std.{Paragraph, Span} -import org.terminal21.client.model.{GlobalEvent, UiEvent} +import org.terminal21.client.model.UiEvent import org.terminal21.model.{CommandEvent, OnChange} import org.terminal21.ui.std.ServerJson From ff2aa75866700b772c4873cab6d41465ccdbdf20 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Sun, 18 Feb 2024 16:18:52 +0000 Subject: [PATCH 060/313] - --- .../main/scala/org/terminal21/client/ConnectedSession.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index 661d8725..3340ea43 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -101,9 +101,9 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se case (onChange: OnChange, h: OnChangeBooleanEventHandler) => h.onChange(onChange.value.toBoolean) case x => logger.error(s"Unknown event handling combination : $x") case None => // nop - val globalEvent = UiEvent(event, modifiedElements(event.key)) - for h <- globalEventHandler do h.onEvent(globalEvent) - events.add(globalEvent) + val globalEvent = UiEvent(event, modifiedElements(event.key)) + for h <- globalEventHandler do h.onEvent(globalEvent) + events.add(globalEvent) catch case t: Throwable => logger.error(s"Session ${session.id}: An error occurred while handling $event", t) From c6c3b1bb8cb3e306b50dd01f7330df8e4b0c9016 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Sun, 18 Feb 2024 16:40:44 +0000 Subject: [PATCH 061/313] - --- .../src/main/scala/tests/LoginForm.scala | 4 ++-- .../scala/tests/StateSessionStateBug.scala | 2 +- .../src/main/scala/tests/chakra/Forms.scala | 4 ++-- .../components/chakra/ChakraElement.scala | 20 ++++++++++--------- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/LoginForm.scala b/end-to-end-tests/src/main/scala/tests/LoginForm.scala index 7be2feb2..d8d5a652 100644 --- a/end-to-end-tests/src/main/scala/tests/LoginForm.scala +++ b/end-to-end-tests/src/main/scala/tests/LoginForm.scala @@ -10,9 +10,9 @@ import org.terminal21.client.components.chakra.* .connect: session => given ConnectedSession = session - val emailInput = Input(`type` = "email", value = "my@email.com") + val emailInput = Input(`type` = "email", defaultValue = "my@email.com") val submitButton = Button(text = "Submit") - val passwordInput = Input(`type` = "password", value = "mysecret") + val passwordInput = Input(`type` = "password", defaultValue = "mysecret") val okIcon = CheckCircleIcon(color = Some("green")) val notOkIcon = WarningTwoIcon(color = Some("red")) val emailRightAddon = InputRightAddon().withChildren(okIcon) diff --git a/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala b/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala index 063b7f8f..2fbc899f 100644 --- a/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala +++ b/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala @@ -31,7 +31,7 @@ import java.util.Date ), Seq( "Date - Input", - Input(value = date.toString) + Input(defaultValue = date.toString) ), Seq( "Date - Std Input", diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala b/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala index f693b29c..e3fe5036 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala @@ -13,7 +13,7 @@ object Forms: val emailRightAddOn = InputRightAddon().withChildren(okIcon) - val email = Input(`type` = "email", value = "my@email.com") + val email = Input(`type` = "email", defaultValue = "my@email.com") email.onChange: newValue => Seq( status.withText(s"email input new value = $newValue, verify email.value = ${email.current.value}"), @@ -37,7 +37,7 @@ object Forms: Option_(text = "Second", value = "2") ) - val password = Input(`type` = "password", value = "mysecret") + val password = Input(`type` = "password", defaultValue = "mysecret") val dob = Input(`type` = "datetime-local") dob.onChange: newValue => status.withText(s"dob = $newValue , verify dob.value = ${dob.current.value}").renderChanges() diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala index 3a4d5c03..cc46572a 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala @@ -226,19 +226,21 @@ case class Input( placeholder: String = "", size: String = "md", variant: Option[String] = None, - value: String = "", + defaultValue: String = "", + valueReceived: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[Input] with HasEventHandler with OnChangeEventHandler.CanHandleOnChangeEvent[Input]: - override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = newValue => session.modified(copy(value = newValue)) - override def withStyle(v: Map[String, Any]) = copy(style = v) - def withKey(v: String) = copy(key = v) - def withType(v: String) = copy(`type` = v) - def withPlaceholder(v: String) = copy(placeholder = v) - def withSize(v: String) = copy(size = v) - def withVariant(v: Option[String]) = copy(variant = v) - def withValue(v: String) = copy(value = v) + override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = newValue => session.modified(copy(valueReceived = Some(newValue))) + override def withStyle(v: Map[String, Any]): Input = copy(style = v) + def withKey(v: String): Input = copy(key = v) + def withType(v: String): Input = copy(`type` = v) + def withPlaceholder(v: String): Input = copy(placeholder = v) + def withSize(v: String): Input = copy(size = v) + def withVariant(v: Option[String]): Input = copy(variant = v) + def withDefaultValue(v: String): Input = copy(defaultValue = v) + def value: String = valueReceived.getOrElse(defaultValue) case class InputGroup( key: String = Keys.nextKey, From d7c343a44ab38b619291d3193c97192d90e86f1d Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Sun, 18 Feb 2024 16:46:04 +0000 Subject: [PATCH 062/313] - --- .../client/components/chakra/ChakraElement.scala | 8 ++++---- .../org/terminal21/client/ConnectedSessionTest.scala | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala index cc46572a..ba513875 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala @@ -149,7 +149,7 @@ case class SimpleGrid( case class Editable( key: String = Keys.nextKey, defaultValue: String = "", - value: String = "", + valueReceived: Option[String] = None, // use value instead style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil ) extends ChakraElement[Editable] @@ -157,12 +157,12 @@ case class Editable( with HasChildren[Editable] with OnChangeEventHandler.CanHandleOnChangeEvent[Editable]: override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = - newValue => session.modified(copy(value = newValue)) + newValue => session.modified(copy(valueReceived = Some(newValue))) override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withDefaultValue(v: String) = copy(defaultValue = v) - def withValue(v: String) = copy(value = v) + def value = valueReceived.getOrElse(defaultValue) case class EditablePreview(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty) extends ChakraElement[EditablePreview]: override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -227,7 +227,7 @@ case class Input( size: String = "md", variant: Option[String] = None, defaultValue: String = "", - valueReceived: Option[String] = None, + valueReceived: Option[String] = None, // use value instead style: Map[String, Any] = Map.empty ) extends ChakraElement[Input] with HasEventHandler diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala index cedd6739..67c0fe2f 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala @@ -25,8 +25,8 @@ class ConnectedSessionTest extends AnyFunSuiteLike: connectedSession.clear() it.toList should be( List( - UiEvent(event1, editable.withValue("v1")), - UiEvent(event2, editable.withValue("v2")) + UiEvent(event1, editable.copy(valueReceived = Some("v1"))), + UiEvent(event2, editable.copy(valueReceived = Some("v2"))) ) ) @@ -37,7 +37,7 @@ class ConnectedSessionTest extends AnyFunSuiteLike: var received = Option.empty[CommandEvent] connectedSession.withGlobalEventHandler: ge => received = Some(ge.event) - ge.receivedBy should be(editable.withValue("new value")) + ge.receivedBy should be(editable.copy(valueReceived = Some("new value"))) val event = OnChange(editable.key, "new value") connectedSession.fireEvent(event) received should be(Some(event)) From d6f59beced9fde30ba9e6af96d552660a1ebef66 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Sun, 18 Feb 2024 16:50:25 +0000 Subject: [PATCH 063/313] - --- end-to-end-tests/src/main/scala/tests/chakra/Forms.scala | 2 +- .../client/components/chakra/ChakraElement.scala | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala b/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala index e3fe5036..b565c988 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala @@ -20,7 +20,7 @@ object Forms: if newValue.contains("@") then emailRightAddOn.withChildren(okIcon) else emailRightAddOn.withChildren(notOkIcon) ).renderChanges() - val description = Textarea(placeholder = "Please enter a few things about you") + val description = Textarea(placeholder = "Please enter a few things about you", defaultValue = "desc") description.onChange: newValue => status.withText(s"description input new value = $newValue, verify description.value = ${description.current.value}").renderChanges() diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala index ba513875..f8f2230e 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala @@ -1386,19 +1386,21 @@ case class Textarea( placeholder: String = "", size: String = "md", variant: Option[String] = None, - value: String = "", + defaultValue: String = "", + valueReceived: Option[String] = None, // use value instead style: Map[String, Any] = Map.empty ) extends ChakraElement[Textarea] with HasEventHandler with OnChangeEventHandler.CanHandleOnChangeEvent[Textarea]: - override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = newValue => session.modified(copy(value = newValue)) + override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = newValue => session.modified(copy(valueReceived = Some(newValue))) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withType(v: String) = copy(`type` = v) def withPlaceholder(v: String) = copy(placeholder = v) def withSize(v: String) = copy(size = v) def withVariant(v: Option[String]) = copy(variant = v) - def withValue(v: String) = copy(value = v) + def withDefaultValue(v: String) = copy(defaultValue = v) + def value = valueReceived.getOrElse(defaultValue) /** https://chakra-ui.com/docs/components/switch */ From aa036434aa5715728283b8e03b9ae16fd26bb2b5 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Sun, 18 Feb 2024 16:52:58 +0000 Subject: [PATCH 064/313] - --- .../terminal21/client/components/chakra/ChakraElement.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala index f8f2230e..f8a884fb 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala @@ -316,7 +316,7 @@ case class Radio( case class RadioGroup( key: String = Keys.nextKey, defaultValue: String = "", - valueV: Option[String] = None, + valueV: Option[String] = None, // use value style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil ) extends ChakraElement[RadioGroup] @@ -1410,7 +1410,7 @@ case class Switch( defaultChecked: Boolean = false, isDisabled: Boolean = false, style: Map[String, Any] = Map.empty, - checkedV: Option[Boolean] = None + checkedV: Option[Boolean] = None // use checked ) extends ChakraElement[Switch] with HasEventHandler with OnChangeBooleanEventHandler.CanHandleOnChangeEvent[Switch]: From d6df3b78d1d0be2395fe49e1b6a3c298a9b95b1d Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Sun, 18 Feb 2024 17:20:30 +0000 Subject: [PATCH 065/313] - --- end-to-end-tests/src/main/scala/tests/LoginForm.scala | 11 +++++++---- .../org/terminal21/client/ConnectedSession.scala | 7 ++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/LoginForm.scala b/end-to-end-tests/src/main/scala/tests/LoginForm.scala index d8d5a652..4d991fa2 100644 --- a/end-to-end-tests/src/main/scala/tests/LoginForm.scala +++ b/end-to-end-tests/src/main/scala/tests/LoginForm.scala @@ -37,16 +37,19 @@ import org.terminal21.client.components.chakra.* ).render() case class PersonSubmitted(email: String, isValidEmail: Boolean, pwd: String, isSubmitted: Boolean, userClosedSession: Boolean) + + def validate(p: PersonSubmitted): Unit = + println(p) + val emailAddon = if p.isValidEmail then emailRightAddon.withChildren(okIcon) else emailRightAddon.withChildren(notOkIcon) + emailAddon.renderChanges() + val p = session.eventIterator .map: e => println(e) val email = emailInput.current.value val pwd = passwordInput.current.value PersonSubmitted(email, email.contains("@"), pwd, e.isTarget(submitButton), e.isSessionClose) - .tapEach: p => - println(p) - val emailAddon = if p.isValidEmail then emailRightAddon.withChildren(okIcon) else emailRightAddon.withChildren(notOkIcon) - emailAddon.renderChanges() + .tapEach(validate) .dropWhile(p => !(p.isSubmitted && p.isValidEmail) && !p.userClosedSession) .next() println("Result:" + p) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index 3340ea43..fca6c495 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -116,9 +116,10 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se sessionsService.setSessionJsonState(session, j) def renderChanges(es: UiElement*): Unit = - for e <- es.flatMap(_.flat) do modified(e) - val j = toJson(es) - sessionsService.changeSessionJsonState(session, j) + if !isClosed then + for e <- es.flatMap(_.flat) do modified(e) + val j = toJson(es) + sessionsService.changeSessionJsonState(session, j) private def toJson(elements: Seq[UiElement]): ServerJson = val flat = elements.flatMap(_.flat) From 8e6b69fb5b4a7d2e54b93926666ff2e5cfcc0a11 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Sun, 18 Feb 2024 17:24:23 +0000 Subject: [PATCH 066/313] - --- end-to-end-tests/src/main/scala/tests/chakra/Forms.scala | 2 +- .../client/components/chakra/ChakraElement.scala | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala b/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala index b565c988..296bbe18 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala @@ -32,7 +32,7 @@ object Forms: select1.onChange: newValue => status.withText(s"select1 input new value = $newValue, verify select1.value = ${select1.current.value}").renderChanges() - val select2 = Select(value = "1", bg = Some("tomato"), color = Some("black"), borderColor = Some("yellow")).withChildren( + val select2 = Select(defaultValue = "1", bg = Some("tomato"), color = Some("black"), borderColor = Some("yellow")).withChildren( Option_(text = "First", value = "1"), Option_(text = "Second", value = "2") ) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala index f8a884fb..981573fa 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala @@ -1427,7 +1427,8 @@ case class Switch( case class Select( key: String = Keys.nextKey, placeholder: String = "", - value: String = "", + defaultValue: String = "", + valueReceived: Option[String] = None, // use value instead bg: Option[String] = None, color: Option[String] = None, borderColor: Option[String] = None, @@ -1437,15 +1438,16 @@ case class Select( with HasEventHandler with HasChildren[Select] with OnChangeEventHandler.CanHandleOnChangeEvent[Select]: - override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = newValue => session.modified(copy(value = newValue)) + override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = newValue => session.modified(copy(valueReceived = Some(newValue))) override def withStyle(v: Map[String, Any]) = copy(style = v) override def withChildren(cn: UiElement*) = copy(children = cn) def withKey(v: String) = copy(key = v) def withPlaceholder(v: String) = copy(placeholder = v) - def withValue(v: String) = copy(value = v) + def withDefaultValue(v: String) = copy(defaultValue = v) def withBg(v: Option[String]) = copy(bg = v) def withColor(v: Option[String]) = copy(color = v) def withBorderColor(v: Option[String]) = copy(borderColor = v) + def value = valueReceived.getOrElse(defaultValue) case class Option_( key: String = Keys.nextKey, From 584e39e936b3500f720b77ab3f9d97b5f10c8aca Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Sun, 18 Feb 2024 18:10:36 +0000 Subject: [PATCH 067/313] - --- .../terminal21/client/components/chakra/ChakraElement.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala index 981573fa..f8e46605 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala @@ -316,17 +316,17 @@ case class Radio( case class RadioGroup( key: String = Keys.nextKey, defaultValue: String = "", - valueV: Option[String] = None, // use value + valueReceived: Option[String] = None, // use value instead style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil ) extends ChakraElement[RadioGroup] with HasEventHandler with HasChildren[RadioGroup] with OnChangeEventHandler.CanHandleOnChangeEvent[RadioGroup]: - override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = newValue => session.modified(copy(valueV = Some(newValue))) + override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = newValue => session.modified(copy(valueReceived = Some(newValue))) override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) - def value: String = valueV.getOrElse(defaultValue) + def value: String = valueReceived.getOrElse(defaultValue) def withKey(v: String) = copy(key = v) def withDefaultValue(v: String) = copy(defaultValue = v) From 94fa074d171f31e5dc33a5df6c1907acc4e50ca4 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Sun, 18 Feb 2024 18:15:45 +0000 Subject: [PATCH 068/313] - --- .../src/main/scala/tests/StateSessionStateBug.scala | 2 +- .../src/main/scala/tests/StdComponents.scala | 2 +- .../terminal21/client/components/std/StdElement.scala | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala b/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala index 2fbc899f..525ee442 100644 --- a/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala +++ b/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala @@ -35,7 +35,7 @@ import java.util.Date ), Seq( "Date - Std Input", - std.Input(defaultValue = Some(date.toString)) + std.Input(defaultValue = date.toString) ) ) ), diff --git a/end-to-end-tests/src/main/scala/tests/StdComponents.scala b/end-to-end-tests/src/main/scala/tests/StdComponents.scala index b3bfa006..0d8dc93c 100644 --- a/end-to-end-tests/src/main/scala/tests/StdComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/StdComponents.scala @@ -10,7 +10,7 @@ import org.terminal21.client.components.std.* .connect: session => given ConnectedSession = session - val input = Input(defaultValue = Some("Please enter your name")) + val input = Input(defaultValue = "Please enter your name") val output = Paragraph(text = "This will reflect what you type in the input") val cookieValue = Paragraph(text = "This will display the value of the cookie") input.onChange: newValue => diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala index 810388cb..1a71d84a 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala @@ -67,15 +67,15 @@ case class Paragraph( case class Input( key: String = Keys.nextKey, `type`: String = "text", - defaultValue: Option[String] = None, + defaultValue: String = "", style: Map[String, Any] = Map.empty, - value: Option[String] = None + valueReceived: Option[String] = None // use value instead ) extends StdElement[Input] with HasEventHandler with CanHandleOnChangeEvent[Input]: - override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = newValue => session.modified(copy(value = Some(newValue))) + override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = newValue => session.modified(copy(valueReceived = Some(newValue))) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withType(v: String) = copy(`type` = v) - def withDefaultValue(v: Option[String]) = copy(defaultValue = v) - def withValue(v: Option[String]) = copy(value = v) + def withDefaultValue(v: String) = copy(defaultValue = v) + def value = valueReceived.getOrElse(defaultValue) From 21ac923eb511f4aeb68c0faa77cf48cd6d52f250 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Sun, 18 Feb 2024 18:56:37 +0000 Subject: [PATCH 069/313] - --- end-to-end-tests/src/main/scala/tests/LoginForm.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/LoginForm.scala b/end-to-end-tests/src/main/scala/tests/LoginForm.scala index 4d991fa2..bc9c8d5d 100644 --- a/end-to-end-tests/src/main/scala/tests/LoginForm.scala +++ b/end-to-end-tests/src/main/scala/tests/LoginForm.scala @@ -36,7 +36,8 @@ import org.terminal21.client.components.chakra.* submitButton ).render() - case class PersonSubmitted(email: String, isValidEmail: Boolean, pwd: String, isSubmitted: Boolean, userClosedSession: Boolean) + case class PersonSubmitted(email: String, isValidEmail: Boolean, pwd: String, isSubmitted: Boolean, userClosedSession: Boolean): + def isReady: Boolean = (isSubmitted && isValidEmail) || userClosedSession def validate(p: PersonSubmitted): Unit = println(p) @@ -50,6 +51,6 @@ import org.terminal21.client.components.chakra.* val pwd = passwordInput.current.value PersonSubmitted(email, email.contains("@"), pwd, e.isTarget(submitButton), e.isSessionClose) .tapEach(validate) - .dropWhile(p => !(p.isSubmitted && p.isValidEmail) && !p.userClosedSession) + .dropWhile(!_.isReady) .next() println("Result:" + p) From 353468e8f91f38872fc5b8eb3467e4bb3d86d1db Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Sun, 18 Feb 2024 19:29:17 +0000 Subject: [PATCH 070/313] - --- end-to-end-tests/src/main/scala/tests/LoginForm.scala | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/LoginForm.scala b/end-to-end-tests/src/main/scala/tests/LoginForm.scala index bc9c8d5d..83bfd8b6 100644 --- a/end-to-end-tests/src/main/scala/tests/LoginForm.scala +++ b/end-to-end-tests/src/main/scala/tests/LoginForm.scala @@ -36,17 +36,12 @@ import org.terminal21.client.components.chakra.* submitButton ).render() - case class PersonSubmitted(email: String, isValidEmail: Boolean, pwd: String, isSubmitted: Boolean, userClosedSession: Boolean): - def isReady: Boolean = (isSubmitted && isValidEmail) || userClosedSession - def validate(p: PersonSubmitted): Unit = - println(p) val emailAddon = if p.isValidEmail then emailRightAddon.withChildren(okIcon) else emailRightAddon.withChildren(notOkIcon) emailAddon.renderChanges() val p = session.eventIterator .map: e => - println(e) val email = emailInput.current.value val pwd = passwordInput.current.value PersonSubmitted(email, email.contains("@"), pwd, e.isTarget(submitButton), e.isSessionClose) @@ -54,3 +49,7 @@ import org.terminal21.client.components.chakra.* .dropWhile(!_.isReady) .next() println("Result:" + p) + if p.isSubmitted then println("Saving person") else println("Not saving, user closed app") + +case class PersonSubmitted(email: String, isValidEmail: Boolean, pwd: String, isSubmitted: Boolean, userClosedSession: Boolean): + def isReady: Boolean = (isSubmitted && isValidEmail) || userClosedSession From 4a135e286590fee9b4f13a814b18069dfdc8e5d5 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Sun, 18 Feb 2024 20:43:16 +0000 Subject: [PATCH 071/313] - --- end-to-end-tests/src/main/scala/tests/LoginForm.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/end-to-end-tests/src/main/scala/tests/LoginForm.scala b/end-to-end-tests/src/main/scala/tests/LoginForm.scala index 83bfd8b6..49e5d922 100644 --- a/end-to-end-tests/src/main/scala/tests/LoginForm.scala +++ b/end-to-end-tests/src/main/scala/tests/LoginForm.scala @@ -49,7 +49,7 @@ import org.terminal21.client.components.chakra.* .dropWhile(!_.isReady) .next() println("Result:" + p) - if p.isSubmitted then println("Saving person") else println("Not saving, user closed app") + if p.isSubmitted then println("Submitted person information") else println("User closed app") case class PersonSubmitted(email: String, isValidEmail: Boolean, pwd: String, isSubmitted: Boolean, userClosedSession: Boolean): def isReady: Boolean = (isSubmitted && isValidEmail) || userClosedSession From 6ef50012562d3a2a8b9ba7ed4018cbd269e1a576 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 19 Feb 2024 11:50:18 +0000 Subject: [PATCH 072/313] - --- example-scripts/on-click.sc | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/example-scripts/on-click.sc b/example-scripts/on-click.sc index 69c6a57c..6685cfb8 100755 --- a/example-scripts/on-click.sc +++ b/example-scripts/on-click.sc @@ -8,16 +8,19 @@ import org.terminal21.model.SessionOptions Sessions .withNewSession("on-click-example", "On Click Handler") - .andOptions(SessionOptions.LeaveOpenWhenTerminated) + .andOptions(SessionOptions.LeaveOpenWhenTerminated) // leave the app UI visible after we terminate so that we can see the final "Button clicked" message .connect: session => given ConnectedSession = session - @volatile var exit = false val msg = Paragraph(text = "Waiting for user to click the button") - val button = Button(text = "Please click me").onClick: () => - msg.withText("Button clicked.").renderChanges() - exit = true - + val button = Button(text = "Please click me") Seq(msg, button).render() - session.waitTillUserClosesSessionOr(exit) + case class Model(isButtonClicked: Boolean) + + session.eventIterator // get an iterator for all events + .map(e => Model(e.isTarget(button))) // convert it to our model + .dropWhile(!_.isButtonClicked) // ignore all events until the user clicks our button + .nextOption() match + case None => // the user closed the app + case Some(model) => msg.withText(s"Button clicked. Model = $model").renderChanges() // the user for sure clicked the button From 103a6500febd7f0942bcccc04040a1c9f1e1df01 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 19 Feb 2024 14:31:42 +0000 Subject: [PATCH 073/313] - --- Readme.md | 1 + example-scripts/csv-editor.sc | 54 +++++++++++++++------------ example-scripts/on-change.sc | 2 +- example-scripts/read-changed-value.sc | 2 +- example-scripts/textedit.sc | 2 +- 5 files changed, 35 insertions(+), 26 deletions(-) diff --git a/Readme.md b/Readme.md index 6f086768..65a3e7fb 100644 --- a/Readme.md +++ b/Readme.md @@ -172,6 +172,7 @@ Please use the [discussions](https://github.com/kostaskougios/terminal21-restapi - session builders refactoring for more flexible creation of sessions - QuickTabs - bug fix for old react state re-rendering on new session +- event iterators allows idiomatic handling of events ## Version 0.21 diff --git a/example-scripts/csv-editor.sc b/example-scripts/csv-editor.sc index 92bb11e6..8c3b81a6 100755 --- a/example-scripts/csv-editor.sc +++ b/example-scripts/csv-editor.sc @@ -9,6 +9,8 @@ import org.terminal21.client.* import java.util.concurrent.atomic.AtomicBoolean import org.terminal21.client.components.* +import org.terminal21.client.model.* +import org.terminal21.model.* import org.terminal21.model.SessionOptions // use the chakra components for menus, forms etc, https://chakra-ui.com/docs/components // The scala case classes : https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala @@ -39,10 +41,11 @@ val initialCsvMap = csv.zipWithIndex .map: (col, x) => ((x, y), col) .toMap -val csvMap = TrieMap.empty[(Int, Int), String] ++ initialCsvMap +type Coords = (Int, Int) +type CsvMap = Map[Coords, String] // save the map back to the csv file -def saveCsvMap() = +def saveCsvMap(csvMap: CsvMap): Unit = val coords = csvMap.keySet val maxX = coords.map(_._1).max val maxY = coords.map(_._2).max @@ -56,9 +59,6 @@ def saveCsvMap() = .mkString("\n") FileUtils.writeStringToFile(file, s, "UTF-8") - // this will be set to true when we have to exit -val exitFlag = new AtomicBoolean(false) - Sessions .withNewSession(s"csv-editor-$fileName", s"CsvEdit: $fileName") .connect: session => @@ -66,24 +66,19 @@ Sessions val status = Box() val saveAndExit = Button(text = "Save & Exit") - .onClick: () => - saveCsvMap() - status.withText("Csv file saved, exiting.").renderChanges() - exitFlag.set(true) - val exit = Button(text = "Exit Without Saving") - .onClick: () => - exitFlag.set(true) - def newEditable(x: Int, y: Int, value: String) = + def newEditable(value: String) = Editable(defaultValue = value) .withChildren( EditablePreview(), EditableInput() ) - .onChange: newValue => - csvMap((x, y)) = newValue - status.withText(s"($x,$y) value changed to $newValue").renderChanges() + + case class Cell(coords: Coords, editable: Editable) + val tableCells = csv.zipWithIndex.map: (row, y) => + row.zipWithIndex.map: (column, x) => + Cell((x, y), newEditable(column)) Seq( TableContainer().withChildren( @@ -92,11 +87,8 @@ Sessions TableCaption(text = "Please edit the csv contents above and click save to save and exit"), Thead(), Tbody( - children = csv.zipWithIndex.map: (row, y) => - Tr( - children = row.zipWithIndex.map: (column, x) => - Td().withChildren(newEditable(x, y, column)) - ) + children = tableCells.map: rowCells => + Tr(children = rowCells.map(c => Td().withChildren(c.editable))) ) ) ), @@ -108,5 +100,21 @@ Sessions ).render() println(s"Now open ${session.uiUrl} to view the UI") - // wait for one of the save/exit buttons to be pressed. - session.waitTillUserClosesSessionOr(exitFlag.get()) + + val editableToCoords = tableCells.flatten.map(cell => (cell.editable.key, cell.coords)).toMap + + case class EditorState(saveAndExitClicked: Boolean, exitWithoutSavingClicked: Boolean, csvMap: CsvMap): + def terminated = saveAndExitClicked || exitWithoutSavingClicked + + session.eventIterator + .scanLeft(EditorState(false, false, csvMap = initialCsvMap)): + case (state, UiEvent(OnChange(key, value), receivedBy)) if editableToCoords.contains(key) => + state.copy(csvMap = state.csvMap + (editableToCoords(key) -> value)) + case (state, event) => state.copy(saveAndExitClicked = event.isTarget(saveAndExit), exitWithoutSavingClicked = event.isTarget(exit)) + .dropWhile(!_.terminated) + .take(1) + .filter(_.saveAndExitClicked) + .foreach: state => + saveCsvMap(state.csvMap) + status.withText("Csv file saved, exiting.").renderChanges() + Thread.sleep(1000) diff --git a/example-scripts/on-change.sc b/example-scripts/on-change.sc index 7650f5c8..3c8f93a2 100755 --- a/example-scripts/on-change.sc +++ b/example-scripts/on-change.sc @@ -11,7 +11,7 @@ Sessions given ConnectedSession = session val output = Paragraph(text = "Please modify the email.") - val email = Input(`type` = "email", value = "my@email.com").onChange: v => + val email = Input(`type` = "email", defaultValue = "my@email.com").onChange: v => output.withText(s"Email value : $v").renderChanges() Seq( diff --git a/example-scripts/read-changed-value.sc b/example-scripts/read-changed-value.sc index 4e899edc..e47778c1 100755 --- a/example-scripts/read-changed-value.sc +++ b/example-scripts/read-changed-value.sc @@ -10,7 +10,7 @@ Sessions .connect: session => given ConnectedSession = session - val email = Input(`type` = "email", value = "my@email.com") + val email = Input(`type` = "email", defaultValue = "my@email.com") val output = Box() Seq( diff --git a/example-scripts/textedit.sc b/example-scripts/textedit.sc index b6c4a787..025cd95b 100755 --- a/example-scripts/textedit.sc +++ b/example-scripts/textedit.sc @@ -38,7 +38,7 @@ Sessions // we will wait till the user clicks the "Exit" menu, this latch makes sure the main thread of the app waits. val exitLatch = new CountDownLatch(1) // the main editor area. - val editor = Textarea(value = contents) + val editor = Textarea(defaultValue = contents) // This will display a "saved" badge for a second when the user saves the file val status = Badge() // This will display an asterisk when the contents of the file are changed in the editor From 45f972094814876e110a58efe15a87983889a2e9 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 19 Feb 2024 14:45:54 +0000 Subject: [PATCH 074/313] - --- example-scripts/csv-editor.sc | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/example-scripts/csv-editor.sc b/example-scripts/csv-editor.sc index 8c3b81a6..14eaec31 100755 --- a/example-scripts/csv-editor.sc +++ b/example-scripts/csv-editor.sc @@ -103,14 +103,17 @@ Sessions val editableToCoords = tableCells.flatten.map(cell => (cell.editable.key, cell.coords)).toMap - case class EditorState(saveAndExitClicked: Boolean, exitWithoutSavingClicked: Boolean, csvMap: CsvMap): + case class EditorState(saveAndExitClicked: Boolean, exitWithoutSavingClicked: Boolean, csvMap: CsvMap, changed: Option[Coords]): def terminated = saveAndExitClicked || exitWithoutSavingClicked session.eventIterator - .scanLeft(EditorState(false, false, csvMap = initialCsvMap)): + .scanLeft(EditorState(false, false, csvMap = initialCsvMap, None)): case (state, UiEvent(OnChange(key, value), receivedBy)) if editableToCoords.contains(key) => - state.copy(csvMap = state.csvMap + (editableToCoords(key) -> value)) - case (state, event) => state.copy(saveAndExitClicked = event.isTarget(saveAndExit), exitWithoutSavingClicked = event.isTarget(exit)) + val coords = editableToCoords(key) + state.copy(csvMap = state.csvMap + (coords -> value), changed = Some(coords)) + case (state, event) => state.copy(saveAndExitClicked = event.isTarget(saveAndExit), exitWithoutSavingClicked = event.isTarget(exit), changed = None) + .tapEach: state => + for coords <- state.changed do status.withText(s"Changed value at $coords, new value is ${state.csvMap(coords)}").renderChanges() .dropWhile(!_.terminated) .take(1) .filter(_.saveAndExitClicked) From 46e24d93d2bc2cd15ba308ca8112adef6826a66c Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 19 Feb 2024 15:00:46 +0000 Subject: [PATCH 075/313] - --- .../scala/tests/StateSessionStateBug.scala | 4 ++-- .../main/scala/tests/chakra/DataDisplay.scala | 4 ++-- example-scripts/csv-editor.sc | 17 +++++--------- .../serverapp/bundled/AppManager.scala | 2 +- .../serverapp/bundled/ServerStatusApp.scala | 14 ++++++------ .../sparklib/endtoend/SparkBasics.scala | 12 +++++----- .../client/components/chakra/QuickTable.scala | 22 +++++++++---------- 7 files changed, 34 insertions(+), 41 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala b/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala index 525ee442..f1d9cf64 100644 --- a/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala +++ b/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala @@ -18,8 +18,8 @@ import java.util.Date Seq( Paragraph(text = s"Now: $date"), QuickTable() - .headers("Title", "Value") - .rows( + .withHeaders("Title", "Value") + .withRows( Seq( Seq( "Date - Editable", diff --git a/end-to-end-tests/src/main/scala/tests/chakra/DataDisplay.scala b/end-to-end-tests/src/main/scala/tests/chakra/DataDisplay.scala index 6954315c..aeba6cbd 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/DataDisplay.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/DataDisplay.scala @@ -14,9 +14,9 @@ object DataDisplay: Th(text = "multiply by", isNumeric = true) ) val quickTable1 = QuickTable() - .headers("id", "name") + .withHeaders("id", "name") .caption("Quick Table Caption") - .rows( + .withRows( Seq( Seq(1, "Kostas"), Seq(2, "Andreas") diff --git a/example-scripts/csv-editor.sc b/example-scripts/csv-editor.sc index 14eaec31..80e1244b 100755 --- a/example-scripts/csv-editor.sc +++ b/example-scripts/csv-editor.sc @@ -81,17 +81,12 @@ Sessions Cell((x, y), newEditable(column)) Seq( - TableContainer().withChildren( - Table(variant = "striped", colorScheme = Some("teal"), size = "mg") - .withChildren( - TableCaption(text = "Please edit the csv contents above and click save to save and exit"), - Thead(), - Tbody( - children = tableCells.map: rowCells => - Tr(children = rowCells.map(c => Td().withChildren(c.editable))) - ) - ) - ), + QuickTable(variant = "striped", colorScheme = "teal", size = "mg") + .withCaption("Please edit the csv contents above and click save to save and exit") + .withRows( + tableCells.toSeq.map: rowCells => + rowCells.map(_.editable) + ), HStack().withChildren( saveAndExit, exit, diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala index f0a56ce5..eecf03aa 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala @@ -24,7 +24,7 @@ class AppManager(serverSideSessions: ServerSideSessions, fiberExecutor: FiberExe val appsTable = QuickTable( caption = Some("Apps installed on the server"), rows = appRows - ).headers("App Name", "Description") + ).withHeaders("App Name", "Description") Seq( appsTable diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 77cf5329..09ad0a22 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -38,8 +38,8 @@ class ServerStatusAppInternal(serverSideSessions: ServerSideSessions, sessionsSe val runtime = Runtime.getRuntime val jvmTable = QuickTable(caption = Some("JVM")) - .headers("Property", "Value", "Actions") - .rows( + .withHeaders("Property", "Value", "Actions") + .withRows( Seq( Seq("Free Memory", toMb(runtime.freeMemory()), ""), Seq("Max Memory", toMb(runtime.maxMemory()), ""), @@ -58,7 +58,7 @@ class ServerStatusAppInternal(serverSideSessions: ServerSideSessions, sessionsSe rows = sessions.map: session => Seq(Text(text = session.id), Text(text = session.name), if session.isOpen then CheckIcon() else NotAllowedIcon(), actionsFor(session)) ) - .headers("Id", "Name", "Is Open", "Actions") + .withHeaders("Id", "Name", "Is Open", "Actions") Seq(jvmTable, sessionsTable).render() @@ -91,8 +91,8 @@ class ViewServerState(session: ConnectedSession): val rootKeyPanel = Seq( QuickTable() .withCaption("Root Keys") - .headers("Root Key") - .rows( + .withHeaders("Root Key") + .withRows( sj.rootKeys.sorted.map(k => Seq(k)) ) ) @@ -100,8 +100,8 @@ class ViewServerState(session: ConnectedSession): val keyTreePanel = Seq( QuickTable() .withCaption("Key Tree") - .headers("Key", "Component Json", "Children") - .rows( + .withHeaders("Key", "Component Json", "Children") + .withRows( sj.keyTree.toSeq.sortBy(_._1).map((k, v) => Seq(k, sj.elements(k).noSpaces, v.mkString(", "))) ) ) diff --git a/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala b/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala index 46f910ba..d6cdc76e 100644 --- a/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala +++ b/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala @@ -25,25 +25,25 @@ import scala.util.Using val headers = Seq("id", "name", "path", "numOfLines", "numOfWords", "createdDate", "timestamp") - val sortedFilesTable = QuickTable().headers(headers: _*).caption("Files sorted by createdDate and numOfWords") - val codeFilesTable = QuickTable().headers(headers: _*).caption("Unsorted files") + val sortedFilesTable = QuickTable().withHeaders(headers: _*).caption("Files sorted by createdDate and numOfWords") + val codeFilesTable = QuickTable().withHeaders(headers: _*).caption("Unsorted files") val sortedSourceFilesDS = sortedSourceFiles(sourceFiles()) val sortedCalc = sortedSourceFilesDS.visualize("Sorted files", sortedFilesTable): results => val tableRows = results.take(3).toList.map(_.toData) - sortedFilesTable.rows(tableRows) + sortedFilesTable.withRows(tableRows) val codeFilesCalculation = sourceFiles().visualize("Code files", codeFilesTable): results => val dt = results.take(3).toList - codeFilesTable.rows(dt.map(_.toData)) + codeFilesTable.withRows(dt.map(_.toData)) - val sortedFilesTableDF = QuickTable().headers(headers: _*).caption("Files sorted by createdDate and numOfWords ASC and as DF") + val sortedFilesTableDF = QuickTable().withHeaders(headers: _*).caption("Files sorted by createdDate and numOfWords ASC and as DF") val sortedCalcAsDF = sourceFiles() .sort($"createdDate".asc, $"numOfWords".asc) .toDF() .visualize("Sorted files DF", sortedFilesTableDF): results => val tableRows = results.take(4).toList - sortedFilesTableDF.rows(tableRows.toUiTable) + sortedFilesTableDF.withRows(tableRows.toUiTable) val chart = ResponsiveLine( data = Seq( diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala index 8f80b566..76ff9b2a 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala @@ -14,14 +14,12 @@ case class QuickTable( rows: Seq[Seq[UiElement]] = Nil ) extends UiComponent with HasStyle[QuickTable]: - def withKey(v: String) = copy(key = v) - def withVariant(v: String) = copy(variant = v) - def withColorScheme(v: String) = copy(colorScheme = v) - def withSize(v: String) = copy(size = v) - def withCaption(v: Option[String]) = copy(caption = v) - def withCaption(v: String) = copy(caption = Some(v)) - def withHeaders(v: Seq[UiElement]) = copy(headers = v) - def withRows(v: Seq[Seq[UiElement]]) = copy(rows = v) + def withKey(v: String) = copy(key = v) + def withVariant(v: String) = copy(variant = v) + def withColorScheme(v: String) = copy(colorScheme = v) + def withSize(v: String) = copy(size = v) + def withCaption(v: Option[String]) = copy(caption = v) + def withCaption(v: String) = copy(caption = Some(v)) override lazy val rendered: Seq[UiElement] = val head = Thead(key = key + "-th", children = Seq(Tr(children = headers.map(h => Th(children = Seq(h)))))) @@ -40,8 +38,8 @@ case class QuickTable( val tableContainer = TableContainer(key = key + "-tc", style = style, children = Seq(table)) Seq(tableContainer) - def headers(headers: String*): QuickTable = copy(headers = headers.map(h => Text(text = h))) - def headersElements(headers: UiElement*): QuickTable = copy(headers = headers) + def withHeaders(headers: String*): QuickTable = copy(headers = headers.map(h => Text(text = h))) + def withHeadersElements(headers: UiElement*): QuickTable = copy(headers = headers) /** @param data * A mix of plain types or UiElement. If it is a UiElement, it will be rendered otherwise if it is anything else the `.toString` method will be used to @@ -49,11 +47,11 @@ case class QuickTable( * @return * QuickTable */ - def rows(data: Seq[Seq[Any]]): QuickTable = copy(rows = data.map(_.map: + def withRows(data: Seq[Seq[Any]]): QuickTable = copy(rows = data.map(_.map: case u: UiElement => u case c => Text(text = c.toString) )) - def rowsElements(data: Seq[Seq[UiElement]]): QuickTable = copy(rows = data) + def withRowsElements(data: Seq[Seq[UiElement]]): QuickTable = copy(rows = data) def caption(text: String): QuickTable = copy(caption = Some(text)) override def withStyle(v: Map[String, Any]): QuickTable = copy(style = v) From 035c0b06b436d249501efd8db2925cce4ea65d78 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 19 Feb 2024 15:12:35 +0000 Subject: [PATCH 076/313] - --- .../serverapp/bundled/AppManager.scala | 19 +++++++++++++++++-- .../serverapp/bundled/SettingsApp.scala | 11 +---------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala index eecf03aa..773ea3c9 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala @@ -4,6 +4,7 @@ import functions.fibers.FiberExecutor import org.terminal21.client.ConnectedSession import org.terminal21.client.components.* import org.terminal21.client.components.chakra.* +import org.terminal21.client.components.std.{Header1, Paragraph, Span} import org.terminal21.model.SessionOptions import org.terminal21.server.Dependencies import org.terminal21.serverapp.{ServerSideApp, ServerSideSessions} @@ -22,12 +23,26 @@ class AppManager(serverSideSessions: ServerSideSessions, fiberExecutor: FiberExe startApp(app) Seq[UiElement](link, Text(text = app.description)) val appsTable = QuickTable( - caption = Some("Apps installed on the server"), + caption = Some("Apps installed on the server, click one to run it."), rows = appRows ).withHeaders("App Name", "Description") Seq( - appsTable + Header1(text = "Terminal 21 Manager"), + Paragraph( + text = """ + |Here you can run all the installed apps on the server.""".stripMargin + ), + appsTable, + Paragraph().withChildren( + Span(text = "Have a question? Please ask at "), + Link( + text = "terminal21's discussion board ", + href = "https://github.com/kostaskougios/terminal21-restapi/discussions", + color = Some("teal.500"), + isExternal = Some(true) + ).withChildren(ExternalLinkIcon(mx = Some("2px"))) + ) ).render() session.waitTillUserClosesSession() diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala index ebf356f9..3ef4d5a2 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala @@ -25,15 +25,6 @@ class SettingsApp extends ServerSideApp: class SettingsAppInstance(using session: ConnectedSession): def run() = Seq( - ThemeToggle(), - Paragraph(style = Map("margin" -> "25px")).withChildren( - Span(text = "Have a question? Please ask at "), - Link( - text = "terminal21's discussion board ", - href = "https://github.com/kostaskougios/terminal21-restapi/discussions", - color = Some("teal.500"), - isExternal = Some(true) - ).withChildren(ExternalLinkIcon(mx = Some("2px"))) - ) + ThemeToggle() ).render() session.waitTillUserClosesSession() From e83c593a7c87d543002d5b076d46c6da00ac3882 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 19 Feb 2024 15:46:13 +0000 Subject: [PATCH 077/313] - --- example-scripts/csv-editor.sc | 60 ++++++++--------------------------- 1 file changed, 13 insertions(+), 47 deletions(-) diff --git a/example-scripts/csv-editor.sc b/example-scripts/csv-editor.sc index 80e1244b..abd0ecc9 100755 --- a/example-scripts/csv-editor.sc +++ b/example-scripts/csv-editor.sc @@ -7,18 +7,15 @@ // always import these import org.terminal21.client.* -import java.util.concurrent.atomic.AtomicBoolean import org.terminal21.client.components.* import org.terminal21.client.model.* import org.terminal21.model.* -import org.terminal21.model.SessionOptions // use the chakra components for menus, forms etc, https://chakra-ui.com/docs/components // The scala case classes : https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala import org.terminal21.client.components.chakra.* import org.apache.commons.io.FileUtils import java.io.File -import scala.collection.concurrent.TrieMap if args.length != 1 then throw new IllegalArgumentException( @@ -31,33 +28,7 @@ val contents = if file.exists() then FileUtils.readFileToString(file, "UTF-8") else "type,damage points,hit points\nmage,10dp,20hp\nwarrior,20dp,30hp" -println(s"Contents: $contents") -val csv = contents.split("\n").map(_.split(",")) - -// store the csv data in a more usable Map -val initialCsvMap = csv.zipWithIndex - .flatMap: (row, y) => - row.zipWithIndex - .map: (col, x) => - ((x, y), col) - .toMap - -type Coords = (Int, Int) -type CsvMap = Map[Coords, String] -// save the map back to the csv file -def saveCsvMap(csvMap: CsvMap): Unit = - val coords = csvMap.keySet - val maxX = coords.map(_._1).max - val maxY = coords.map(_._2).max - - val s = (0 to maxX) - .map: y => - (0 to maxY) - .map: x => - csvMap.getOrElse((x, y), "") - .mkString(",") - .mkString("\n") - FileUtils.writeStringToFile(file, s, "UTF-8") +val csv = contents.split("\n").map(_.split(",").toSeq).toSeq Sessions .withNewSession(s"csv-editor-$fileName", s"CsvEdit: $fileName") @@ -75,18 +46,15 @@ Sessions EditableInput() ) - case class Cell(coords: Coords, editable: Editable) - val tableCells = csv.zipWithIndex.map: (row, y) => - row.zipWithIndex.map: (column, x) => - Cell((x, y), newEditable(column)) + val tableCells = + csv.map: row => + row.map: column => + newEditable(column) Seq( QuickTable(variant = "striped", colorScheme = "teal", size = "mg") .withCaption("Please edit the csv contents above and click save to save and exit") - .withRows( - tableCells.toSeq.map: rowCells => - rowCells.map(_.editable) - ), + .withRows(tableCells), HStack().withChildren( saveAndExit, exit, @@ -96,23 +64,21 @@ Sessions println(s"Now open ${session.uiUrl} to view the UI") - val editableToCoords = tableCells.flatten.map(cell => (cell.editable.key, cell.coords)).toMap - - case class EditorState(saveAndExitClicked: Boolean, exitWithoutSavingClicked: Boolean, csvMap: CsvMap, changed: Option[Coords]): + case class EditorState(saveAndExitClicked: Boolean, exitWithoutSavingClicked: Boolean, changed: Option[String]): def terminated = saveAndExitClicked || exitWithoutSavingClicked session.eventIterator - .scanLeft(EditorState(false, false, csvMap = initialCsvMap, None)): - case (state, UiEvent(OnChange(key, value), receivedBy)) if editableToCoords.contains(key) => - val coords = editableToCoords(key) - state.copy(csvMap = state.csvMap + (coords -> value), changed = Some(coords)) + .scanLeft(EditorState(false, false, None)): + case (state, UiEvent(OnChange(key, value), receivedBy)) => + state.copy(changed = Some(value)) case (state, event) => state.copy(saveAndExitClicked = event.isTarget(saveAndExit), exitWithoutSavingClicked = event.isTarget(exit), changed = None) .tapEach: state => - for coords <- state.changed do status.withText(s"Changed value at $coords, new value is ${state.csvMap(coords)}").renderChanges() + for value <- state.changed do status.withText(s"Changed a cell value to $value").renderChanges() .dropWhile(!_.terminated) .take(1) .filter(_.saveAndExitClicked) .foreach: state => - saveCsvMap(state.csvMap) + val data = tableCells.map(_.map(_.current.value).mkString(",")).mkString("\n") + FileUtils.writeStringToFile(file, data, "UTF-8") status.withText("Csv file saved, exiting.").renderChanges() Thread.sleep(1000) From a8334c6cb1838f579903a299d4d1f6b2f8f81acd Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 19 Feb 2024 18:16:15 +0000 Subject: [PATCH 078/313] - --- .../src/main/scala/tests/LoginForm.scala | 50 +++++++++-------- .../terminal21/client/ConnectedSession.scala | 2 +- .../org/terminal21/client/Controller.scala | 54 +++++++++++++++++++ 3 files changed, 82 insertions(+), 24 deletions(-) create mode 100644 terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala diff --git a/end-to-end-tests/src/main/scala/tests/LoginForm.scala b/end-to-end-tests/src/main/scala/tests/LoginForm.scala index 49e5d922..b7acb882 100644 --- a/end-to-end-tests/src/main/scala/tests/LoginForm.scala +++ b/end-to-end-tests/src/main/scala/tests/LoginForm.scala @@ -1,6 +1,6 @@ package tests -import org.terminal21.client.{ConnectedSession, Sessions} +import org.terminal21.client.{ConnectedSession, Controller, Sessions} import org.terminal21.client.components.* import org.terminal21.client.components.chakra.* @@ -10,9 +10,11 @@ import org.terminal21.client.components.chakra.* .connect: session => given ConnectedSession = session - val emailInput = Input(`type` = "email", defaultValue = "my@email.com") + val initialModel = Login("my@email.com", "mysecret") + + val emailInput = Input(`type` = "email", defaultValue = initialModel.email) val submitButton = Button(text = "Submit") - val passwordInput = Input(`type` = "password", defaultValue = "mysecret") + val passwordInput = Input(`type` = "password", defaultValue = initialModel.pwd) val okIcon = CheckCircleIcon(color = Some("green")) val notOkIcon = WarningTwoIcon(color = Some("red")) val emailRightAddon = InputRightAddon().withChildren(okIcon) @@ -25,31 +27,33 @@ import org.terminal21.client.components.chakra.* emailInput, emailRightAddon ), - FormControl().withChildren( - FormLabel(text = "Password"), - InputGroup().withChildren( + QuickFormControl() + .withLabel("Password") + .withHelperText("Don't share with anyone") + .withInputGroup( InputLeftAddon().withChildren(ViewOffIcon()), passwordInput ), - FormHelperText(text = "Don't share with anyone") - ), submitButton ).render() - def validate(p: PersonSubmitted): Unit = - val emailAddon = if p.isValidEmail then emailRightAddon.withChildren(okIcon) else emailRightAddon.withChildren(notOkIcon) - emailAddon.renderChanges() + def validate(login: Login): InputRightAddon = + if login.isValidEmail then emailRightAddon.withChildren(okIcon) else emailRightAddon.withChildren(notOkIcon) - val p = session.eventIterator - .map: e => - val email = emailInput.current.value - val pwd = passwordInput.current.value - PersonSubmitted(email, email.contains("@"), pwd, e.isTarget(submitButton), e.isSessionClose) - .tapEach(validate) - .dropWhile(!_.isReady) - .next() - println("Result:" + p) - if p.isSubmitted then println("Submitted person information") else println("User closed app") + Controller(initialModel) + .onClick(submitButton): clickEvent => + clickEvent.handled.withShouldTerminate(clickEvent.model.isValidEmail) + .onChange(emailInput): changeEvent => + val newEmail = changeEvent.newValue + val model = changeEvent.model.copy(email = newEmail) + changeEvent.handled + .withModel(model) + .withRenderChanges(validate(model)) + .iterator + .toList + .lastOption match + case Some(login) if !session.isClosed => println(s"Login will be processed: $login") + case _ => println("Login cancelled") -case class PersonSubmitted(email: String, isValidEmail: Boolean, pwd: String, isSubmitted: Boolean, userClosedSession: Boolean): - def isReady: Boolean = (isSubmitted && isValidEmail) || userClosedSession +private case class Login(email: String, pwd: String): + def isValidEmail: Boolean = email.contains("@") diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index fca6c495..784fd163 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -116,7 +116,7 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se sessionsService.setSessionJsonState(session, j) def renderChanges(es: UiElement*): Unit = - if !isClosed then + if !isClosed && es.nonEmpty then for e <- es.flatMap(_.flat) do modified(e) val j = toJson(es) sessionsService.changeSessionJsonState(session, j) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala new file mode 100644 index 00000000..bef3e952 --- /dev/null +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -0,0 +1,54 @@ +package org.terminal21.client + +import org.terminal21.client.OnChangeEventHandler.CanHandleOnChangeEvent +import org.terminal21.client.OnClickEventHandler.CanHandleOnClickEvent +import org.terminal21.client.components.UiElement +import org.terminal21.client.model.UiEvent +import org.terminal21.model.{OnChange, OnClick} + +class Controller[M]( + session: ConnectedSession, + initialModel: M, + clickHandlers: Map[String, ControllerClickEvent[M] => HandledEvent[M]], + changeHandlers: Map[String, ControllerChangeEvent[M] => HandledEvent[M]] +): + def onClick(e: UiElement & CanHandleOnClickEvent[_])(handler: ControllerClickEvent[M] => HandledEvent[M]) = + new Controller(session, initialModel, clickHandlers + (e.key -> handler), changeHandlers) + + def onChange(e: UiElement & CanHandleOnChangeEvent[_])(handler: ControllerChangeEvent[M] => HandledEvent[M]) = + new Controller(session, initialModel, clickHandlers, changeHandlers + (e.key -> handler)) + + def iterator: Iterator[M] = + session.eventIterator + .takeWhile(!_.isSessionClose) + .scanLeft(HandledEvent(initialModel, Nil, false)): + case (h, UiEvent(OnClick(key), receivedBy)) if clickHandlers.contains(key) => + val handler = clickHandlers(key) + val handled = handler(ControllerClickEvent(receivedBy, h.model)) + session.renderChanges(handled.renderChanges: _*) + handled + case (h, UiEvent(OnChange(key, value), receivedBy)) if changeHandlers.contains(key) => + val handler = changeHandlers(key) + val handled = handler(ControllerChangeEvent(receivedBy, h.model, value)) + session.renderChanges(handled.renderChanges: _*) + handled + case x => throw new IllegalStateException(s"unexpected state $x") + .takeWhile(!_.shouldTerminate) + .map(_.model) + +object Controller: + def apply[M](initialModel: M)(using session: ConnectedSession) = new Controller(session, initialModel, Map.empty, Map.empty) + +trait ControllerEvent[M]: + def model: M + def handled: HandledEvent[M] = HandledEvent(model, Nil, false) + +case class ControllerClickEvent[M](clicked: UiElement, model: M) extends ControllerEvent[M] + +case class ControllerChangeEvent[M](changed: UiElement, model: M, newValue: String) extends ControllerEvent[M] + +case class HandledEvent[M](model: M, renderChanges: Seq[UiElement], shouldTerminate: Boolean): + def terminate: HandledEvent[M] = copy(shouldTerminate = true) + def withShouldTerminate(t: Boolean): HandledEvent[M] = copy(shouldTerminate = t) + def withModel(m: M): HandledEvent[M] = copy(model = m) + def withRenderChanges(changed: UiElement*): HandledEvent[M] = copy(renderChanges = changed) From d36484cf59dcebd06187eb14a6dcb069a8d6a46c Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 19 Feb 2024 19:27:19 +0000 Subject: [PATCH 079/313] - --- end-to-end-tests/src/main/scala/tests/LoginForm.scala | 4 +--- .../src/main/scala/org/terminal21/client/Controller.scala | 4 +++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/LoginForm.scala b/end-to-end-tests/src/main/scala/tests/LoginForm.scala index b7acb882..be343589 100644 --- a/end-to-end-tests/src/main/scala/tests/LoginForm.scala +++ b/end-to-end-tests/src/main/scala/tests/LoginForm.scala @@ -49,9 +49,7 @@ import org.terminal21.client.components.chakra.* changeEvent.handled .withModel(model) .withRenderChanges(validate(model)) - .iterator - .toList - .lastOption match + .lastModelOption match case Some(login) if !session.isClosed => println(s"Login will be processed: $login") case _ => println("Login cancelled") diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index bef3e952..239a210f 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -32,10 +32,12 @@ class Controller[M]( val handled = handler(ControllerChangeEvent(receivedBy, h.model, value)) session.renderChanges(handled.renderChanges: _*) handled - case x => throw new IllegalStateException(s"unexpected state $x") + case (handled, _) => handled .takeWhile(!_.shouldTerminate) .map(_.model) + def lastModelOption: Option[M] = iterator.toList.lastOption + object Controller: def apply[M](initialModel: M)(using session: ConnectedSession) = new Controller(session, initialModel, Map.empty, Map.empty) From 7ad8e9f09e8f44072f9cd3ed7a5f86eb9b89746b Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 19 Feb 2024 20:13:49 +0000 Subject: [PATCH 080/313] - --- .../src/main/scala/tests/LoginForm.scala | 8 ++-- .../org/terminal21/client/Controller.scala | 42 +++++++++++-------- .../terminal21/client/model/GlobalEvent.scala | 6 +-- 3 files changed, 31 insertions(+), 25 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/LoginForm.scala b/end-to-end-tests/src/main/scala/tests/LoginForm.scala index be343589..de47d760 100644 --- a/end-to-end-tests/src/main/scala/tests/LoginForm.scala +++ b/end-to-end-tests/src/main/scala/tests/LoginForm.scala @@ -41,14 +41,12 @@ import org.terminal21.client.components.chakra.* if login.isValidEmail then emailRightAddon.withChildren(okIcon) else emailRightAddon.withChildren(notOkIcon) Controller(initialModel) + .onEvent: model => + model.copy(email = emailInput.current.value, pwd = passwordInput.current.value) .onClick(submitButton): clickEvent => clickEvent.handled.withShouldTerminate(clickEvent.model.isValidEmail) .onChange(emailInput): changeEvent => - val newEmail = changeEvent.newValue - val model = changeEvent.model.copy(email = newEmail) - changeEvent.handled - .withModel(model) - .withRenderChanges(validate(model)) + changeEvent.handled.withRenderChanges(validate(changeEvent.model)) .lastModelOption match case Some(login) if !session.isClosed => println(s"Login will be processed: $login") case _ => println("Login cancelled") diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 239a210f..ca9f94ce 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -9,44 +9,52 @@ import org.terminal21.model.{OnChange, OnClick} class Controller[M]( session: ConnectedSession, initialModel: M, + eventHandlers: Seq[M => M], clickHandlers: Map[String, ControllerClickEvent[M] => HandledEvent[M]], changeHandlers: Map[String, ControllerChangeEvent[M] => HandledEvent[M]] ): + def onEvent(handler: M => M) = + new Controller(session, initialModel, eventHandlers :+ handler, clickHandlers, changeHandlers) + def onClick(e: UiElement & CanHandleOnClickEvent[_])(handler: ControllerClickEvent[M] => HandledEvent[M]) = - new Controller(session, initialModel, clickHandlers + (e.key -> handler), changeHandlers) + new Controller(session, initialModel, eventHandlers, clickHandlers + (e.key -> handler), changeHandlers) def onChange(e: UiElement & CanHandleOnChangeEvent[_])(handler: ControllerChangeEvent[M] => HandledEvent[M]) = - new Controller(session, initialModel, clickHandlers, changeHandlers + (e.key -> handler)) + new Controller(session, initialModel, eventHandlers, clickHandlers, changeHandlers + (e.key -> handler)) def iterator: Iterator[M] = session.eventIterator .takeWhile(!_.isSessionClose) - .scanLeft(HandledEvent(initialModel, Nil, false)): - case (h, UiEvent(OnClick(key), receivedBy)) if clickHandlers.contains(key) => - val handler = clickHandlers(key) - val handled = handler(ControllerClickEvent(receivedBy, h.model)) - session.renderChanges(handled.renderChanges: _*) - handled - case (h, UiEvent(OnChange(key, value), receivedBy)) if changeHandlers.contains(key) => - val handler = changeHandlers(key) - val handled = handler(ControllerChangeEvent(receivedBy, h.model, value)) - session.renderChanges(handled.renderChanges: _*) - handled - case (handled, _) => handled + .scanLeft(HandledEvent(initialModel, Nil, false)): (oldHandled, event) => + val newModel = eventHandlers.foldLeft(oldHandled.model): (model, f) => + f(model) + + val h = oldHandled.copy(model = newModel) + val handled = event match + case UiEvent(OnClick(key), receivedBy) if clickHandlers.contains(key) => + val handler = clickHandlers(key) + val handled = handler(ControllerClickEvent(receivedBy, h.model)) + handled + case UiEvent(OnChange(key, value), receivedBy) if changeHandlers.contains(key) => + val handler = changeHandlers(key) + val handled = handler(ControllerChangeEvent(receivedBy, h.model, value)) + handled + case _ => h + session.renderChanges(handled.renderChanges: _*) + handled .takeWhile(!_.shouldTerminate) .map(_.model) def lastModelOption: Option[M] = iterator.toList.lastOption object Controller: - def apply[M](initialModel: M)(using session: ConnectedSession) = new Controller(session, initialModel, Map.empty, Map.empty) + def apply[M](initialModel: M)(using session: ConnectedSession) = new Controller(session, initialModel, Nil, Map.empty, Map.empty) trait ControllerEvent[M]: def model: M def handled: HandledEvent[M] = HandledEvent(model, Nil, false) -case class ControllerClickEvent[M](clicked: UiElement, model: M) extends ControllerEvent[M] - +case class ControllerClickEvent[M](clicked: UiElement, model: M) extends ControllerEvent[M] case class ControllerChangeEvent[M](changed: UiElement, model: M, newValue: String) extends ControllerEvent[M] case class HandledEvent[M](model: M, renderChanges: Seq[UiElement], shouldTerminate: Boolean): diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/model/GlobalEvent.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/model/GlobalEvent.scala index c32c83b0..aa5edaf6 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/model/GlobalEvent.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/model/GlobalEvent.scala @@ -8,9 +8,9 @@ sealed trait GlobalEvent: def isSessionClose: Boolean case class UiEvent(event: CommandEvent, receivedBy: UiElement) extends GlobalEvent: - override def isTarget(e: UiElement): Boolean = e == receivedBy - override def isSessionClose: Boolean = false + override def isTarget(e: UiElement): Boolean = e.key == receivedBy.key + override def isSessionClose: Boolean = false case object SessionClosedEvent extends GlobalEvent: override def isTarget(e: UiElement): Boolean = false - override def isSessionClose: Boolean = true + override def isSessionClose: Boolean = true From fd7ae1a9c2c69e21cca6dc903aab0966a941591f Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 19 Feb 2024 21:03:57 +0000 Subject: [PATCH 081/313] - --- example-scripts/csv-editor.sc | 23 +++++++++---------- .../org/terminal21/client/Controller.scala | 8 +++---- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/example-scripts/csv-editor.sc b/example-scripts/csv-editor.sc index abd0ecc9..b78f5e2f 100755 --- a/example-scripts/csv-editor.sc +++ b/example-scripts/csv-editor.sc @@ -64,21 +64,20 @@ Sessions println(s"Now open ${session.uiUrl} to view the UI") - case class EditorState(saveAndExitClicked: Boolean, exitWithoutSavingClicked: Boolean, changed: Option[String]): + case class EditorState(saveAndExitClicked: Boolean, exitWithoutSavingClicked: Boolean): def terminated = saveAndExitClicked || exitWithoutSavingClicked - session.eventIterator - .scanLeft(EditorState(false, false, None)): - case (state, UiEvent(OnChange(key, value), receivedBy)) => - state.copy(changed = Some(value)) - case (state, event) => state.copy(saveAndExitClicked = event.isTarget(saveAndExit), exitWithoutSavingClicked = event.isTarget(exit), changed = None) - .tapEach: state => - for value <- state.changed do status.withText(s"Changed a cell value to $value").renderChanges() - .dropWhile(!_.terminated) - .take(1) - .filter(_.saveAndExitClicked) - .foreach: state => + Controller(EditorState(false, false)) + .onClick(saveAndExit): event => + event.handled.withModel(event.model.copy(saveAndExitClicked = true)).terminate + .onClick(exit): click => + click.handled.withModel(click.model.copy(exitWithoutSavingClicked = true)).terminate + .onChange(tableCells.flatten*): event => + event.handled.withRenderChanges(status.withText(s"Changed a cell value to ${event.newValue}")) + .lastModelOption match + case Some(state) if state.saveAndExitClicked => val data = tableCells.map(_.map(_.current.value).mkString(",")).mkString("\n") FileUtils.writeStringToFile(file, data, "UTF-8") status.withText("Csv file saved, exiting.").renderChanges() Thread.sleep(1000) + case _ => // just exit diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index ca9f94ce..c61fd021 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -16,11 +16,11 @@ class Controller[M]( def onEvent(handler: M => M) = new Controller(session, initialModel, eventHandlers :+ handler, clickHandlers, changeHandlers) - def onClick(e: UiElement & CanHandleOnClickEvent[_])(handler: ControllerClickEvent[M] => HandledEvent[M]) = - new Controller(session, initialModel, eventHandlers, clickHandlers + (e.key -> handler), changeHandlers) + def onClick(elements: UiElement & CanHandleOnClickEvent[_]*)(handler: ControllerClickEvent[M] => HandledEvent[M]) = + new Controller(session, initialModel, eventHandlers, clickHandlers ++ elements.map(e => e.key -> handler), changeHandlers) - def onChange(e: UiElement & CanHandleOnChangeEvent[_])(handler: ControllerChangeEvent[M] => HandledEvent[M]) = - new Controller(session, initialModel, eventHandlers, clickHandlers, changeHandlers + (e.key -> handler)) + def onChange(elements: UiElement & CanHandleOnChangeEvent[_]*)(handler: ControllerChangeEvent[M] => HandledEvent[M]) = + new Controller(session, initialModel, eventHandlers, clickHandlers, changeHandlers ++ elements.map(e => e.key -> handler)) def iterator: Iterator[M] = session.eventIterator From e4008cc47bba57a547e9f4a74915477118887733 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 20 Feb 2024 01:26:48 +0000 Subject: [PATCH 082/313] - --- .../src/main/scala/tests/LoginForm.scala | 5 +- example-scripts/csv-editor.sc | 13 ++--- .../org/terminal21/client/Controller.scala | 49 ++++++++++++++----- .../terminal21/client/ControllerTest.scala | 32 ++++++++++++ 4 files changed, 76 insertions(+), 23 deletions(-) create mode 100644 terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala diff --git a/end-to-end-tests/src/main/scala/tests/LoginForm.scala b/end-to-end-tests/src/main/scala/tests/LoginForm.scala index de47d760..b88ab0b2 100644 --- a/end-to-end-tests/src/main/scala/tests/LoginForm.scala +++ b/end-to-end-tests/src/main/scala/tests/LoginForm.scala @@ -41,8 +41,9 @@ import org.terminal21.client.components.chakra.* if login.isValidEmail then emailRightAddon.withChildren(okIcon) else emailRightAddon.withChildren(notOkIcon) Controller(initialModel) - .onEvent: model => - model.copy(email = emailInput.current.value, pwd = passwordInput.current.value) + .onEvent: event => + val newModel = event.model.copy(email = emailInput.current.value, pwd = passwordInput.current.value) + event.handled.withModel(newModel) .onClick(submitButton): clickEvent => clickEvent.handled.withShouldTerminate(clickEvent.model.isValidEmail) .onChange(emailInput): changeEvent => diff --git a/example-scripts/csv-editor.sc b/example-scripts/csv-editor.sc index b78f5e2f..d7e4e019 100755 --- a/example-scripts/csv-editor.sc +++ b/example-scripts/csv-editor.sc @@ -64,20 +64,17 @@ Sessions println(s"Now open ${session.uiUrl} to view the UI") - case class EditorState(saveAndExitClicked: Boolean, exitWithoutSavingClicked: Boolean): - def terminated = saveAndExitClicked || exitWithoutSavingClicked - - Controller(EditorState(false, false)) + Controller(false) .onClick(saveAndExit): event => - event.handled.withModel(event.model.copy(saveAndExitClicked = true)).terminate + event.handled.withModel(true).terminate .onClick(exit): click => - click.handled.withModel(click.model.copy(exitWithoutSavingClicked = true)).terminate + click.handled.withModel(false).terminate .onChange(tableCells.flatten*): event => event.handled.withRenderChanges(status.withText(s"Changed a cell value to ${event.newValue}")) .lastModelOption match - case Some(state) if state.saveAndExitClicked => + case Some(true) => val data = tableCells.map(_.map(_.current.value).mkString(",")).mkString("\n") FileUtils.writeStringToFile(file, data, "UTF-8") status.withText("Csv file saved, exiting.").renderChanges() Thread.sleep(1000) - case _ => // just exit + case x => // just exit diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index c61fd021..cd4b61cf 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -3,33 +3,52 @@ package org.terminal21.client import org.terminal21.client.OnChangeEventHandler.CanHandleOnChangeEvent import org.terminal21.client.OnClickEventHandler.CanHandleOnClickEvent import org.terminal21.client.components.UiElement -import org.terminal21.client.model.UiEvent +import org.terminal21.client.model.{GlobalEvent, UiEvent} import org.terminal21.model.{OnChange, OnClick} class Controller[M]( - session: ConnectedSession, + eventIteratorFactory: => Iterator[GlobalEvent], + renderChanges: Seq[UiElement] => Unit, initialModel: M, - eventHandlers: Seq[M => M], + eventHandlers: Seq[ControllerEvent[M] => HandledEvent[M]], clickHandlers: Map[String, ControllerClickEvent[M] => HandledEvent[M]], changeHandlers: Map[String, ControllerChangeEvent[M] => HandledEvent[M]] ): - def onEvent(handler: M => M) = - new Controller(session, initialModel, eventHandlers :+ handler, clickHandlers, changeHandlers) + def onEvent(handler: ControllerEvent[M] => HandledEvent[M]) = + new Controller(eventIteratorFactory, renderChanges, initialModel, eventHandlers :+ handler, clickHandlers, changeHandlers) def onClick(elements: UiElement & CanHandleOnClickEvent[_]*)(handler: ControllerClickEvent[M] => HandledEvent[M]) = - new Controller(session, initialModel, eventHandlers, clickHandlers ++ elements.map(e => e.key -> handler), changeHandlers) + new Controller( + eventIteratorFactory, + renderChanges, + initialModel, + eventHandlers, + clickHandlers ++ elements.map(e => e.key -> handler), + changeHandlers + ) def onChange(elements: UiElement & CanHandleOnChangeEvent[_]*)(handler: ControllerChangeEvent[M] => HandledEvent[M]) = - new Controller(session, initialModel, eventHandlers, clickHandlers, changeHandlers ++ elements.map(e => e.key -> handler)) + new Controller( + eventIteratorFactory, + renderChanges, + initialModel, + eventHandlers, + clickHandlers, + changeHandlers ++ elements.map(e => e.key -> handler) + ) def iterator: Iterator[M] = - session.eventIterator + eventIteratorFactory .takeWhile(!_.isSessionClose) .scanLeft(HandledEvent(initialModel, Nil, false)): (oldHandled, event) => - val newModel = eventHandlers.foldLeft(oldHandled.model): (model, f) => - f(model) + val h = eventHandlers.foldLeft(oldHandled): (h, f) => + event match + case UiEvent(OnClick(_), receivedBy) => + f(ControllerClickEvent(receivedBy, h.model)) + case UiEvent(OnChange(_, value), receivedBy) => + f(ControllerChangeEvent(receivedBy, h.model, value)) + case x => throw new IllegalStateException(s"Unexpected state $x") - val h = oldHandled.copy(model = newModel) val handled = event match case UiEvent(OnClick(key), receivedBy) if clickHandlers.contains(key) => val handler = clickHandlers(key) @@ -40,7 +59,7 @@ class Controller[M]( val handled = handler(ControllerChangeEvent(receivedBy, h.model, value)) handled case _ => h - session.renderChanges(handled.renderChanges: _*) + renderChanges(handled.renderChanges) handled .takeWhile(!_.shouldTerminate) .map(_.model) @@ -48,7 +67,11 @@ class Controller[M]( def lastModelOption: Option[M] = iterator.toList.lastOption object Controller: - def apply[M](initialModel: M)(using session: ConnectedSession) = new Controller(session, initialModel, Nil, Map.empty, Map.empty) + def apply[M](initialModel: M)(using session: ConnectedSession): Controller[M] = + new Controller(session.eventIterator, session.renderChanges, initialModel, Nil, Map.empty, Map.empty) + + def apply[M](initialModel: M, eventIterator: => Iterator[GlobalEvent], renderChanges: Seq[UiElement] => Unit = _ => ()): Controller[M] = + new Controller(eventIterator, renderChanges, initialModel, Nil, Map.empty, Map.empty) trait ControllerEvent[M]: def model: M diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala new file mode 100644 index 00000000..b6a7fcc5 --- /dev/null +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -0,0 +1,32 @@ +package org.terminal21.client + +import org.scalatest.funsuite.AnyFunSuiteLike +import org.terminal21.client.components.chakra.Button +import org.terminal21.client.model.UiEvent +import org.terminal21.model.OnClick +import org.scalatest.matchers.should.Matchers.* + +class ControllerTest extends AnyFunSuiteLike: + val button = Button() + val buttonClick = UiEvent(OnClick(button.key), button) + + test("onEvent is called"): + Controller(0, Iterator(buttonClick)) + .onEvent: event => + if event.model > 1 then event.handled.terminate else event.handled.withModel(event.model + 1) + .iterator + .toList should be(List(0, 1)) + + test("onClick is called"): + Controller(0, Iterator(buttonClick)) + .onClick(button): event => + event.handled.withModel(100).terminate + .iterator + .toList should be(List(0, 100)) + + test("terminate is obeyed and model when terminating is iterated"): + Controller(0, Iterator(buttonClick, buttonClick, buttonClick)) + .onEvent: event => + if event.model > 1 then event.handled.terminate.withModel(100) else event.handled.withModel(event.model + 1) + .iterator + .toList should be(List(0, 1, 2, 100)) From 1ee5aca373f9ce6911744f46aecb2055081d8d64 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 20 Feb 2024 12:03:46 +0000 Subject: [PATCH 083/313] - --- .../src/main/scala/org/terminal21/client/Controller.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index cd4b61cf..95f61cf4 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -61,6 +61,9 @@ class Controller[M]( case _ => h renderChanges(handled.renderChanges) handled + .flatMap: h => + // trick to make sure we take the last state of the model when shouldTerminate=true + if h.shouldTerminate then Seq(h.copy(shouldTerminate = false), h) else Seq(h) .takeWhile(!_.shouldTerminate) .map(_.model) From d631b0d85547705f0e1274b6fb113b2b755e78d7 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 20 Feb 2024 12:20:33 +0000 Subject: [PATCH 084/313] - --- .../org/terminal21/client/ControllerTest.scala | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index b6a7fcc5..28522d35 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -3,12 +3,15 @@ package org.terminal21.client import org.scalatest.funsuite.AnyFunSuiteLike import org.terminal21.client.components.chakra.Button import org.terminal21.client.model.UiEvent -import org.terminal21.model.OnClick +import org.terminal21.model.{OnChange, OnClick} import org.scalatest.matchers.should.Matchers.* +import org.terminal21.client.components.std.Input class ControllerTest extends AnyFunSuiteLike: val button = Button() val buttonClick = UiEvent(OnClick(button.key), button) + val input = Input() + val inputChange = UiEvent(OnChange(input.key, "new-value"), input) test("onEvent is called"): Controller(0, Iterator(buttonClick)) @@ -24,7 +27,14 @@ class ControllerTest extends AnyFunSuiteLike: .iterator .toList should be(List(0, 100)) - test("terminate is obeyed and model when terminating is iterated"): + test("onChange is called"): + Controller(0, Iterator(inputChange)) + .onChange(input): event => + event.handled.withModel(100).terminate + .iterator + .toList should be(List(0, 100)) + + test("terminate is obeyed and latest model state is iterated"): Controller(0, Iterator(buttonClick, buttonClick, buttonClick)) .onEvent: event => if event.model > 1 then event.handled.terminate.withModel(100) else event.handled.withModel(event.model + 1) From b6226448a792b818041273cfb4b809bac4d56097 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 20 Feb 2024 12:44:22 +0000 Subject: [PATCH 085/313] - --- .../org/terminal21/client/Controller.scala | 64 ++++++++++++++----- .../terminal21/client/ControllerTest.scala | 38 +++++++++-- 2 files changed, 78 insertions(+), 24 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 95f61cf4..ca3b3d6d 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -12,29 +12,51 @@ class Controller[M]( initialModel: M, eventHandlers: Seq[ControllerEvent[M] => HandledEvent[M]], clickHandlers: Map[String, ControllerClickEvent[M] => HandledEvent[M]], - changeHandlers: Map[String, ControllerChangeEvent[M] => HandledEvent[M]] + changeHandlers: Map[String, ControllerChangeEvent[M] => HandledEvent[M]], + changeBooleanHandlers: Map[String, ControllerChangeBooleanEvent[M] => HandledEvent[M]] ): def onEvent(handler: ControllerEvent[M] => HandledEvent[M]) = - new Controller(eventIteratorFactory, renderChanges, initialModel, eventHandlers :+ handler, clickHandlers, changeHandlers) + new Controller(eventIteratorFactory, renderChanges, initialModel, eventHandlers :+ handler, clickHandlers, changeHandlers, changeBooleanHandlers) - def onClick(elements: UiElement & CanHandleOnClickEvent[_]*)(handler: ControllerClickEvent[M] => HandledEvent[M]) = + def onClick(element: UiElement & CanHandleOnClickEvent[_])(handler: ControllerClickEvent[M] => HandledEvent[M]): Controller[M] = onClicked(element)(handler) + def onClicked(elements: UiElement & CanHandleOnClickEvent[_]*)(handler: ControllerClickEvent[M] => HandledEvent[M]): Controller[M] = new Controller( eventIteratorFactory, renderChanges, initialModel, eventHandlers, clickHandlers ++ elements.map(e => e.key -> handler), - changeHandlers + changeHandlers, + changeBooleanHandlers ) - def onChange(elements: UiElement & CanHandleOnChangeEvent[_]*)(handler: ControllerChangeEvent[M] => HandledEvent[M]) = + def onChange(element: UiElement & OnChangeEventHandler.CanHandleOnChangeEvent[_])(handler: ControllerChangeEvent[M] => HandledEvent[M]): Controller[M] = + onChanged(element)(handler) + def onChanged(elements: UiElement & OnChangeEventHandler.CanHandleOnChangeEvent[_]*)(handler: ControllerChangeEvent[M] => HandledEvent[M]): Controller[M] = new Controller( eventIteratorFactory, renderChanges, initialModel, eventHandlers, clickHandlers, - changeHandlers ++ elements.map(e => e.key -> handler) + changeHandlers ++ elements.map(e => e.key -> handler), + changeBooleanHandlers + ) + + def onChange(element: UiElement & OnChangeBooleanEventHandler.CanHandleOnChangeEvent[_])(handler: ControllerChangeBooleanEvent[M] => HandledEvent[M]) = + onChangedBoolean(element)(handler) + + def onChangedBoolean( + elements: UiElement & OnChangeBooleanEventHandler.CanHandleOnChangeEvent[_]* + )(handler: ControllerChangeBooleanEvent[M] => HandledEvent[M]) = + new Controller( + eventIteratorFactory, + renderChanges, + initialModel, + eventHandlers, + clickHandlers, + changeHandlers, + changeBooleanHandlers ++ elements.map(e => e.key -> handler) ) def iterator: Iterator[M] = @@ -43,22 +65,29 @@ class Controller[M]( .scanLeft(HandledEvent(initialModel, Nil, false)): (oldHandled, event) => val h = eventHandlers.foldLeft(oldHandled): (h, f) => event match - case UiEvent(OnClick(_), receivedBy) => + case UiEvent(OnClick(_), receivedBy) => f(ControllerClickEvent(receivedBy, h.model)) - case UiEvent(OnChange(_, value), receivedBy) => - f(ControllerChangeEvent(receivedBy, h.model, value)) - case x => throw new IllegalStateException(s"Unexpected state $x") + case UiEvent(OnChange(key, value), receivedBy) => + val e = receivedBy match + case _: OnChangeEventHandler.CanHandleOnChangeEvent[_] => ControllerChangeEvent(receivedBy, h.model, value) + case _: OnChangeBooleanEventHandler.CanHandleOnChangeEvent[_] => ControllerChangeBooleanEvent(receivedBy, h.model, value.toBoolean) + f(e) + case x => throw new IllegalStateException(s"Unexpected state $x") val handled = event match - case UiEvent(OnClick(key), receivedBy) if clickHandlers.contains(key) => + case UiEvent(OnClick(key), receivedBy) if clickHandlers.contains(key) => val handler = clickHandlers(key) val handled = handler(ControllerClickEvent(receivedBy, h.model)) handled - case UiEvent(OnChange(key, value), receivedBy) if changeHandlers.contains(key) => + case UiEvent(OnChange(key, value), receivedBy) if changeHandlers.contains(key) => val handler = changeHandlers(key) val handled = handler(ControllerChangeEvent(receivedBy, h.model, value)) handled - case _ => h + case UiEvent(OnChange(key, value), receivedBy) if changeBooleanHandlers.contains(key) => + val handler = changeBooleanHandlers(key) + val handled = handler(ControllerChangeBooleanEvent(receivedBy, h.model, value.toBoolean)) + handled + case _ => h renderChanges(handled.renderChanges) handled .flatMap: h => @@ -71,17 +100,18 @@ class Controller[M]( object Controller: def apply[M](initialModel: M)(using session: ConnectedSession): Controller[M] = - new Controller(session.eventIterator, session.renderChanges, initialModel, Nil, Map.empty, Map.empty) + new Controller(session.eventIterator, session.renderChanges, initialModel, Nil, Map.empty, Map.empty, Map.empty) def apply[M](initialModel: M, eventIterator: => Iterator[GlobalEvent], renderChanges: Seq[UiElement] => Unit = _ => ()): Controller[M] = - new Controller(eventIterator, renderChanges, initialModel, Nil, Map.empty, Map.empty) + new Controller(eventIterator, renderChanges, initialModel, Nil, Map.empty, Map.empty, Map.empty) trait ControllerEvent[M]: def model: M def handled: HandledEvent[M] = HandledEvent(model, Nil, false) -case class ControllerClickEvent[M](clicked: UiElement, model: M) extends ControllerEvent[M] -case class ControllerChangeEvent[M](changed: UiElement, model: M, newValue: String) extends ControllerEvent[M] +case class ControllerClickEvent[M](clicked: UiElement, model: M) extends ControllerEvent[M] +case class ControllerChangeEvent[M](changed: UiElement, model: M, newValue: String) extends ControllerEvent[M] +case class ControllerChangeBooleanEvent[M](changed: UiElement, model: M, newValue: Boolean) extends ControllerEvent[M] case class HandledEvent[M](model: M, renderChanges: Seq[UiElement], shouldTerminate: Boolean): def terminate: HandledEvent[M] = copy(shouldTerminate = true) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 28522d35..f8c6c906 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -1,17 +1,19 @@ package org.terminal21.client import org.scalatest.funsuite.AnyFunSuiteLike -import org.terminal21.client.components.chakra.Button -import org.terminal21.client.model.UiEvent -import org.terminal21.model.{OnChange, OnClick} import org.scalatest.matchers.should.Matchers.* +import org.terminal21.client.components.chakra.{Button, Checkbox} import org.terminal21.client.components.std.Input +import org.terminal21.client.model.UiEvent +import org.terminal21.model.{OnChange, OnClick} class ControllerTest extends AnyFunSuiteLike: - val button = Button() - val buttonClick = UiEvent(OnClick(button.key), button) - val input = Input() - val inputChange = UiEvent(OnChange(input.key, "new-value"), input) + val button = Button() + val buttonClick = UiEvent(OnClick(button.key), button) + val input = Input() + val inputChange = UiEvent(OnChange(input.key, "new-value"), input) + val checkbox = Checkbox() + val checkBoxChange = UiEvent(OnChange(checkbox.key, "true"), checkbox) test("onEvent is called"): Controller(0, Iterator(buttonClick)) @@ -20,6 +22,21 @@ class ControllerTest extends AnyFunSuiteLike: .iterator .toList should be(List(0, 1)) + test("onEvent is called for change"): + Controller(0, Iterator(inputChange)) + .onEvent: event => + if event.model > 1 then event.handled.terminate else event.handled.withModel(event.model + 1) + .iterator + .toList should be(List(0, 1)) + + test("onEvent is called for change/boolean"): + Controller(0, Iterator(checkBoxChange)) + .onEvent: + case event @ ControllerChangeBooleanEvent(`checkbox`, 0, true) => + if event.model > 1 then event.handled.terminate else event.handled.withModel(event.model + 1) + .iterator + .toList should be(List(0, 1)) + test("onClick is called"): Controller(0, Iterator(buttonClick)) .onClick(button): event => @@ -34,6 +51,13 @@ class ControllerTest extends AnyFunSuiteLike: .iterator .toList should be(List(0, 100)) + test("onChange/boolean is called"): + Controller(0, Iterator(checkBoxChange)) + .onChange(checkbox): event => + event.handled.withModel(100).terminate + .iterator + .toList should be(List(0, 100)) + test("terminate is obeyed and latest model state is iterated"): Controller(0, Iterator(buttonClick, buttonClick, buttonClick)) .onEvent: event => From 21acf78961fd8822eada3c39019f3830d15d545d Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 20 Feb 2024 12:46:48 +0000 Subject: [PATCH 086/313] - --- .../test/scala/org/terminal21/client/ControllerTest.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index f8c6c906..991a11cc 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -24,8 +24,9 @@ class ControllerTest extends AnyFunSuiteLike: test("onEvent is called for change"): Controller(0, Iterator(inputChange)) - .onEvent: event => - if event.model > 1 then event.handled.terminate else event.handled.withModel(event.model + 1) + .onEvent: + case event @ ControllerChangeEvent(`input`, 0, "new-value") => + if event.model > 1 then event.handled.terminate else event.handled.withModel(event.model + 1) .iterator .toList should be(List(0, 1)) From 3b21df13ada2fee53d88fdf98be0ce7121ea5d41 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 20 Feb 2024 12:57:16 +0000 Subject: [PATCH 087/313] - --- .../org/terminal21/client/ControllerTest.scala | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 991a11cc..c4c4b936 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -2,6 +2,7 @@ package org.terminal21.client import org.scalatest.funsuite.AnyFunSuiteLike import org.scalatest.matchers.should.Matchers.* +import org.terminal21.client.components.UiElement import org.terminal21.client.components.chakra.{Button, Checkbox} import org.terminal21.client.components.std.Input import org.terminal21.client.model.UiEvent @@ -65,3 +66,15 @@ class ControllerTest extends AnyFunSuiteLike: if event.model > 1 then event.handled.terminate.withModel(100) else event.handled.withModel(event.model + 1) .iterator .toList should be(List(0, 1, 2, 100)) + + test("changes are rendered"): + var rendered = Seq.empty[UiElement] + def renderer(s: Seq[UiElement]): Unit = rendered = s + + Controller(0, Iterator(buttonClick), renderer) + .onEvent: event => + event.handled.withModel(event.model + 1).withRenderChanges(button.withText("changed")).terminate + .iterator + .toList should be(List(0, 1)) + + rendered should be(Seq(button.withText("changed"))) From 96bbffba7c0017e57211adf30b54d2711380de6c Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 20 Feb 2024 14:24:26 +0000 Subject: [PATCH 088/313] - --- .../src/main/scala/org/terminal21/client/Controller.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index ca3b3d6d..88eb074c 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -88,8 +88,9 @@ class Controller[M]( val handled = handler(ControllerChangeBooleanEvent(receivedBy, h.model, value.toBoolean)) handled case _ => h - renderChanges(handled.renderChanges) handled + .tapEach: handled => + renderChanges(handled.renderChanges) .flatMap: h => // trick to make sure we take the last state of the model when shouldTerminate=true if h.shouldTerminate then Seq(h.copy(shouldTerminate = false), h) else Seq(h) From 5db2bf7cdf8dc4757f008f4b4a0b63e59f6f8acd Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 20 Feb 2024 14:57:40 +0000 Subject: [PATCH 089/313] - --- example-scripts/csv-editor.sc | 64 ++++++++++++++++++++++------------- 1 file changed, 40 insertions(+), 24 deletions(-) diff --git a/example-scripts/csv-editor.sc b/example-scripts/csv-editor.sc index d7e4e019..ffb3f79f 100755 --- a/example-scripts/csv-editor.sc +++ b/example-scripts/csv-editor.sc @@ -34,23 +34,28 @@ Sessions .withNewSession(s"csv-editor-$fileName", s"CsvEdit: $fileName") .connect: session => given ConnectedSession = session + println(s"Now open ${session.uiUrl} to view the UI") + val editor = new CsvEditor(csv) + editor.run() - val status = Box() - val saveAndExit = Button(text = "Save & Exit") - val exit = Button(text = "Exit Without Saving") +class CsvEditor(csv: Seq[Seq[String]])(using session: ConnectedSession): + val saveAndExit = Button(text = "Save & Exit") + val exit = Button(text = "Exit Without Saving") + val status = Box() - def newEditable(value: String) = - Editable(defaultValue = value) - .withChildren( - EditablePreview(), - EditableInput() - ) + val tableCells = + csv.map: row => + row.map: column => + newEditable(column) - val tableCells = - csv.map: row => - row.map: column => - newEditable(column) + def run(): Unit = + components.render() + if processEvents then + save() + status.withText("Csv file saved, exiting.").renderChanges() + Thread.sleep(1000) + def components: Seq[UiElement] = Seq( QuickTable(variant = "striped", colorScheme = "teal", size = "mg") .withCaption("Please edit the csv contents above and click save to save and exit") @@ -60,21 +65,32 @@ Sessions exit, status ) - ).render() + ) - println(s"Now open ${session.uiUrl} to view the UI") + /** @return + * true if the user clicked "Save", false if the user clicked "Exit" or closed the session + */ + def processEvents: Boolean = + registerCsvEditorEventHandlers(Controller(false)).lastModelOption.getOrElse(false) + + def save(): Unit = + val data = currentCsvValue + FileUtils.writeStringToFile(file, data, "UTF-8") + + def currentCsvValue: String = tableCells.map(_.map(_.current.value).mkString(",")).mkString("\n") + + private def newEditable(value: String) = + Editable(defaultValue = value) + .withChildren( + EditablePreview(), + EditableInput() + ) - Controller(false) + def registerCsvEditorEventHandlers(controller: Controller[Boolean]) = + controller .onClick(saveAndExit): event => event.handled.withModel(true).terminate .onClick(exit): click => click.handled.withModel(false).terminate - .onChange(tableCells.flatten*): event => + .onChanged(tableCells.flatten*): event => event.handled.withRenderChanges(status.withText(s"Changed a cell value to ${event.newValue}")) - .lastModelOption match - case Some(true) => - val data = tableCells.map(_.map(_.current.value).mkString(",")).mkString("\n") - FileUtils.writeStringToFile(file, data, "UTF-8") - status.withText("Csv file saved, exiting.").renderChanges() - Thread.sleep(1000) - case x => // just exit From 6c4a04ee50e6e5308321c05a99413236becbc268 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 20 Feb 2024 15:11:13 +0000 Subject: [PATCH 090/313] - --- .../src/main/scala/tests/LoginForm.scala | 92 ++++++++++--------- 1 file changed, 51 insertions(+), 41 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/LoginForm.scala b/end-to-end-tests/src/main/scala/tests/LoginForm.scala index b88ab0b2..e3d3fe80 100644 --- a/end-to-end-tests/src/main/scala/tests/LoginForm.scala +++ b/end-to-end-tests/src/main/scala/tests/LoginForm.scala @@ -4,53 +4,63 @@ import org.terminal21.client.{ConnectedSession, Controller, Sessions} import org.terminal21.client.components.* import org.terminal21.client.components.chakra.* -@main def loginForm(): Unit = +@main def loginFormApp(): Unit = Sessions .withNewSession("login-form", "Login Form") .connect: session => given ConnectedSession = session - - val initialModel = Login("my@email.com", "mysecret") - - val emailInput = Input(`type` = "email", defaultValue = initialModel.email) - val submitButton = Button(text = "Submit") - val passwordInput = Input(`type` = "password", defaultValue = initialModel.pwd) - val okIcon = CheckCircleIcon(color = Some("green")) - val notOkIcon = WarningTwoIcon(color = Some("red")) - val emailRightAddon = InputRightAddon().withChildren(okIcon) - Seq( - QuickFormControl() - .withLabel("Email address") - .withHelperText("We'll never share your email.") - .withInputGroup( - InputLeftAddon().withChildren(EmailIcon()), - emailInput, - emailRightAddon - ), - QuickFormControl() - .withLabel("Password") - .withHelperText("Don't share with anyone") - .withInputGroup( - InputLeftAddon().withChildren(ViewOffIcon()), - passwordInput - ), - submitButton - ).render() - - def validate(login: Login): InputRightAddon = - if login.isValidEmail then emailRightAddon.withChildren(okIcon) else emailRightAddon.withChildren(notOkIcon) - - Controller(initialModel) - .onEvent: event => - val newModel = event.model.copy(email = emailInput.current.value, pwd = passwordInput.current.value) - event.handled.withModel(newModel) - .onClick(submitButton): clickEvent => - clickEvent.handled.withShouldTerminate(clickEvent.model.isValidEmail) - .onChange(emailInput): changeEvent => - changeEvent.handled.withRenderChanges(validate(changeEvent.model)) - .lastModelOption match + val form = new LoginForm() + form.run() match case Some(login) if !session.isClosed => println(s"Login will be processed: $login") case _ => println("Login cancelled") +class LoginForm(using session: ConnectedSession): + private val initialModel = Login("my@email.com", "mysecret") + private val okIcon = CheckCircleIcon(color = Some("green")) + private val notOkIcon = WarningTwoIcon(color = Some("red")) + private val emailRightAddon = InputRightAddon().withChildren(okIcon) + private val emailInput = Input(`type` = "email", defaultValue = initialModel.email) + private val submitButton = Button(text = "Submit") + private val passwordInput = Input(`type` = "password", defaultValue = initialModel.pwd) + + def run(): Option[Login] = + components.render() + processEvents + + def components: Seq[UiElement] = + Seq( + QuickFormControl() + .withLabel("Email address") + .withHelperText("We'll never share your email.") + .withInputGroup( + InputLeftAddon().withChildren(EmailIcon()), + emailInput, + emailRightAddon + ), + QuickFormControl() + .withLabel("Password") + .withHelperText("Don't share with anyone") + .withInputGroup( + InputLeftAddon().withChildren(ViewOffIcon()), + passwordInput + ), + submitButton + ) + + def processEvents: Option[Login] = registerHandlers(Controller(initialModel)).lastModelOption + + def registerHandlers(controller: Controller[Login]): Controller[Login] = + controller + .onEvent: event => + val newModel = event.model.copy(email = emailInput.current.value, pwd = passwordInput.current.value) + event.handled.withModel(newModel) + .onClick(submitButton): clickEvent => + clickEvent.handled.withShouldTerminate(clickEvent.model.isValidEmail) + .onChange(emailInput): changeEvent => + changeEvent.handled.withRenderChanges(validate(changeEvent.model)) + + private def validate(login: Login): InputRightAddon = + if login.isValidEmail then emailRightAddon.withChildren(okIcon) else emailRightAddon.withChildren(notOkIcon) + private case class Login(email: String, pwd: String): def isValidEmail: Boolean = email.contains("@") From b78fbceffbae0789dde933e6ca476e017217439f Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 20 Feb 2024 16:55:07 +0000 Subject: [PATCH 091/313] - --- build.sbt | 2 +- .../src/main/scala/tests/LoginForm.scala | 35 ++++++++--------- .../src/test/scala/tests/LoginFormTest.scala | 38 +++++++++++++++++++ .../client/components/AnyElement.scala | 4 ++ .../org/terminal21/model/CommandEvent.scala | 8 ++++ .../terminal21/client/ConnectedSession.scala | 2 + .../client/components/UiElement.scala | 2 +- .../terminal21/client/model/GlobalEvent.scala | 8 +++- 8 files changed, 77 insertions(+), 22 deletions(-) create mode 100644 end-to-end-tests/src/test/scala/tests/LoginFormTest.scala create mode 100644 terminal21-server-client-common/src/main/scala/org/terminal21/client/components/AnyElement.scala diff --git a/build.sbt b/build.sbt index 52abdab2..bd987c4a 100644 --- a/build.sbt +++ b/build.sbt @@ -160,7 +160,7 @@ lazy val `end-to-end-tests` = project publish := {}, libraryDependencies ++= Seq(ScalaTest, LogBack) ) - .dependsOn(`terminal21-ui-std`, `terminal21-nivo`, `terminal21-mathjax`) + .dependsOn(`terminal21-ui-std` % "compile->compile;test->test", `terminal21-nivo`, `terminal21-mathjax`) lazy val `terminal21-nivo` = project .settings( diff --git a/end-to-end-tests/src/main/scala/tests/LoginForm.scala b/end-to-end-tests/src/main/scala/tests/LoginForm.scala index e3d3fe80..7d8f7b70 100644 --- a/end-to-end-tests/src/main/scala/tests/LoginForm.scala +++ b/end-to-end-tests/src/main/scala/tests/LoginForm.scala @@ -15,17 +15,17 @@ import org.terminal21.client.components.chakra.* case _ => println("Login cancelled") class LoginForm(using session: ConnectedSession): - private val initialModel = Login("my@email.com", "mysecret") - private val okIcon = CheckCircleIcon(color = Some("green")) - private val notOkIcon = WarningTwoIcon(color = Some("red")) - private val emailRightAddon = InputRightAddon().withChildren(okIcon) - private val emailInput = Input(`type` = "email", defaultValue = initialModel.email) - private val submitButton = Button(text = "Submit") - private val passwordInput = Input(`type` = "password", defaultValue = initialModel.pwd) + private val initialModel = Login("my@email.com", "mysecret") + val okIcon = CheckCircleIcon(color = Some("green")) + val notOkIcon = WarningTwoIcon(color = Some("red")) + val emailRightAddon = InputRightAddon().withChildren(okIcon) + val emailInput = Input(`type` = "email", defaultValue = initialModel.email) + val submitButton = Button(text = "Submit") + val passwordInput = Input(`type` = "password", defaultValue = initialModel.pwd) def run(): Option[Login] = components.render() - processEvents + controller.lastModelOption def components: Seq[UiElement] = Seq( @@ -47,17 +47,14 @@ class LoginForm(using session: ConnectedSession): submitButton ) - def processEvents: Option[Login] = registerHandlers(Controller(initialModel)).lastModelOption - - def registerHandlers(controller: Controller[Login]): Controller[Login] = - controller - .onEvent: event => - val newModel = event.model.copy(email = emailInput.current.value, pwd = passwordInput.current.value) - event.handled.withModel(newModel) - .onClick(submitButton): clickEvent => - clickEvent.handled.withShouldTerminate(clickEvent.model.isValidEmail) - .onChange(emailInput): changeEvent => - changeEvent.handled.withRenderChanges(validate(changeEvent.model)) + def controller: Controller[Login] = Controller(initialModel) + .onEvent: event => + val newModel = event.model.copy(email = emailInput.current.value, pwd = passwordInput.current.value) + event.handled.withModel(newModel) + .onClick(submitButton): clickEvent => + clickEvent.handled.withShouldTerminate(clickEvent.model.isValidEmail) + .onChange(emailInput): changeEvent => + changeEvent.handled.withRenderChanges(validate(changeEvent.model)) private def validate(login: Login): InputRightAddon = if login.isValidEmail then emailRightAddon.withChildren(okIcon) else emailRightAddon.withChildren(notOkIcon) diff --git a/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala b/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala new file mode 100644 index 00000000..49a788c6 --- /dev/null +++ b/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala @@ -0,0 +1,38 @@ +package tests + +import org.scalatest.funsuite.AnyFunSuiteLike +import org.terminal21.client.{ConnectedSession, ConnectedSessionMock, Controller} +import org.scalatest.matchers.should.Matchers.* +import org.terminal21.client.model.* +import org.terminal21.client.components.* +import org.terminal21.model.CommandEvent + +class LoginFormTest extends AnyFunSuiteLike: + test("renders email input"): + new App: + allComponents should contain(form.emailInput) + + test("renders password input"): + new App: + allComponents should contain(form.passwordInput) + + test("renders submit button"): + new App: + allComponents should contain(form.submitButton) + + test("user submits validated data"): + new App: + form.components.render() + val eventsIt = form.controller.iterator // get the iterator before we fire the events, otherwise the iterator will be empty + session.fireEvents( + CommandEvent.onChange(form.emailInput, "an@email.com"), + CommandEvent.onChange(form.passwordInput, "secret"), + CommandEvent.onClick(form.submitButton) + ) + + eventsIt.toList.lastOption should be(Some(Login("an@email.com", "secret"))) + + class App: + given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock + val form = new LoginForm(using session) + def allComponents = form.components.flatMap(_.flat) diff --git a/terminal21-server-client-common/src/main/scala/org/terminal21/client/components/AnyElement.scala b/terminal21-server-client-common/src/main/scala/org/terminal21/client/components/AnyElement.scala new file mode 100644 index 00000000..b1fe25f8 --- /dev/null +++ b/terminal21-server-client-common/src/main/scala/org/terminal21/client/components/AnyElement.scala @@ -0,0 +1,4 @@ +package org.terminal21.client.components + +trait AnyElement: + def key: String diff --git a/terminal21-server-client-common/src/main/scala/org/terminal21/model/CommandEvent.scala b/terminal21-server-client-common/src/main/scala/org/terminal21/model/CommandEvent.scala index 216d6fe2..cc6d473e 100644 --- a/terminal21-server-client-common/src/main/scala/org/terminal21/model/CommandEvent.scala +++ b/terminal21-server-client-common/src/main/scala/org/terminal21/model/CommandEvent.scala @@ -1,8 +1,16 @@ package org.terminal21.model +import org.terminal21.client.components.AnyElement + sealed trait CommandEvent: def key: String +object CommandEvent: + def onClick(receivedBy: AnyElement): OnClick = OnClick(receivedBy.key) + def onChange(receivedBy: AnyElement, value: String): OnChange = OnChange(receivedBy.key, value) + def onChange(receivedBy: AnyElement, value: Boolean): OnChange = OnChange(receivedBy.key, value.toString) + def sessionClosed: SessionClosed = SessionClosed("-") + case class OnClick(key: String) extends CommandEvent case class OnChange(key: String, value: String) extends CommandEvent diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index 784fd163..c62d13a4 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -83,6 +83,8 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se */ def removeGlobalEventHandler(): Unit = globalEventHandler = None + def fireEvents(events: CommandEvent*): Unit = for e <- events do fireEvent(e) + def fireEvent(event: CommandEvent): Unit = try event match diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala index 34eff4d6..86212b34 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala @@ -2,7 +2,7 @@ package org.terminal21.client.components import org.terminal21.client.{ConnectedSession, EventHandler} -trait UiElement: +trait UiElement extends AnyElement: def key: String /** @return diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/model/GlobalEvent.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/model/GlobalEvent.scala index aa5edaf6..12fac343 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/model/GlobalEvent.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/model/GlobalEvent.scala @@ -1,12 +1,18 @@ package org.terminal21.client.model import org.terminal21.client.components.UiElement -import org.terminal21.model.CommandEvent +import org.terminal21.model.{CommandEvent, OnChange, OnClick} sealed trait GlobalEvent: def isTarget(e: UiElement): Boolean def isSessionClose: Boolean +object GlobalEvent: + def onClick(receivedBy: UiElement): UiEvent = UiEvent(OnClick(receivedBy.key), receivedBy) + def onChange(receivedBy: UiElement, value: String): UiEvent = UiEvent(OnChange(receivedBy.key, value), receivedBy) + def onChangeEvent(receivedBy: UiElement, value: Boolean): UiEvent = UiEvent(OnChange(receivedBy.key, value.toString), receivedBy) + def sessionClosedEvent: GlobalEvent = SessionClosedEvent + case class UiEvent(event: CommandEvent, receivedBy: UiElement) extends GlobalEvent: override def isTarget(e: UiElement): Boolean = e.key == receivedBy.key override def isSessionClose: Boolean = false From c3814533bae936ce131f51e06817045ea9f788bf Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 20 Feb 2024 16:56:00 +0000 Subject: [PATCH 092/313] - --- .../scala/org/terminal21/client/components/AnyElement.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/terminal21-server-client-common/src/main/scala/org/terminal21/client/components/AnyElement.scala b/terminal21-server-client-common/src/main/scala/org/terminal21/client/components/AnyElement.scala index b1fe25f8..3a1fe334 100644 --- a/terminal21-server-client-common/src/main/scala/org/terminal21/client/components/AnyElement.scala +++ b/terminal21-server-client-common/src/main/scala/org/terminal21/client/components/AnyElement.scala @@ -1,4 +1,6 @@ package org.terminal21.client.components +/** Base trait for any renderable element that has a key + */ trait AnyElement: def key: String From 9988bb847dd5769a23d65b59646c28b528eb07d7 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 20 Feb 2024 16:57:34 +0000 Subject: [PATCH 093/313] - --- .../org/terminal21/client/Controller.scala | 3 --- .../terminal21/client/ControllerTest.scala | 21 +++++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 88eb074c..85c51f10 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -103,9 +103,6 @@ object Controller: def apply[M](initialModel: M)(using session: ConnectedSession): Controller[M] = new Controller(session.eventIterator, session.renderChanges, initialModel, Nil, Map.empty, Map.empty, Map.empty) - def apply[M](initialModel: M, eventIterator: => Iterator[GlobalEvent], renderChanges: Seq[UiElement] => Unit = _ => ()): Controller[M] = - new Controller(eventIterator, renderChanges, initialModel, Nil, Map.empty, Map.empty, Map.empty) - trait ControllerEvent[M]: def model: M def handled: HandledEvent[M] = HandledEvent(model, Nil, false) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index c4c4b936..b737f5cc 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -5,7 +5,7 @@ import org.scalatest.matchers.should.Matchers.* import org.terminal21.client.components.UiElement import org.terminal21.client.components.chakra.{Button, Checkbox} import org.terminal21.client.components.std.Input -import org.terminal21.client.model.UiEvent +import org.terminal21.client.model.{GlobalEvent, UiEvent} import org.terminal21.model.{OnChange, OnClick} class ControllerTest extends AnyFunSuiteLike: @@ -16,15 +16,18 @@ class ControllerTest extends AnyFunSuiteLike: val checkbox = Checkbox() val checkBoxChange = UiEvent(OnChange(checkbox.key, "true"), checkbox) + def newController[M](initialModel: M, eventIterator: => Iterator[GlobalEvent], renderChanges: Seq[UiElement] => Unit = _ => ()): Controller[M] = + new Controller(eventIterator, renderChanges, initialModel, Nil, Map.empty, Map.empty, Map.empty) + test("onEvent is called"): - Controller(0, Iterator(buttonClick)) + newController(0, Iterator(buttonClick)) .onEvent: event => if event.model > 1 then event.handled.terminate else event.handled.withModel(event.model + 1) .iterator .toList should be(List(0, 1)) test("onEvent is called for change"): - Controller(0, Iterator(inputChange)) + newController(0, Iterator(inputChange)) .onEvent: case event @ ControllerChangeEvent(`input`, 0, "new-value") => if event.model > 1 then event.handled.terminate else event.handled.withModel(event.model + 1) @@ -32,7 +35,7 @@ class ControllerTest extends AnyFunSuiteLike: .toList should be(List(0, 1)) test("onEvent is called for change/boolean"): - Controller(0, Iterator(checkBoxChange)) + newController(0, Iterator(checkBoxChange)) .onEvent: case event @ ControllerChangeBooleanEvent(`checkbox`, 0, true) => if event.model > 1 then event.handled.terminate else event.handled.withModel(event.model + 1) @@ -40,28 +43,28 @@ class ControllerTest extends AnyFunSuiteLike: .toList should be(List(0, 1)) test("onClick is called"): - Controller(0, Iterator(buttonClick)) + newController(0, Iterator(buttonClick)) .onClick(button): event => event.handled.withModel(100).terminate .iterator .toList should be(List(0, 100)) test("onChange is called"): - Controller(0, Iterator(inputChange)) + newController(0, Iterator(inputChange)) .onChange(input): event => event.handled.withModel(100).terminate .iterator .toList should be(List(0, 100)) test("onChange/boolean is called"): - Controller(0, Iterator(checkBoxChange)) + newController(0, Iterator(checkBoxChange)) .onChange(checkbox): event => event.handled.withModel(100).terminate .iterator .toList should be(List(0, 100)) test("terminate is obeyed and latest model state is iterated"): - Controller(0, Iterator(buttonClick, buttonClick, buttonClick)) + newController(0, Iterator(buttonClick, buttonClick, buttonClick)) .onEvent: event => if event.model > 1 then event.handled.terminate.withModel(100) else event.handled.withModel(event.model + 1) .iterator @@ -71,7 +74,7 @@ class ControllerTest extends AnyFunSuiteLike: var rendered = Seq.empty[UiElement] def renderer(s: Seq[UiElement]): Unit = rendered = s - Controller(0, Iterator(buttonClick), renderer) + newController(0, Iterator(buttonClick), renderer) .onEvent: event => event.handled.withModel(event.model + 1).withRenderChanges(button.withText("changed")).terminate .iterator From 0fc4ec5939adac6b58d8960e5e62b1d3fe36650a Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 20 Feb 2024 20:02:12 +0000 Subject: [PATCH 094/313] - --- .../org/terminal21/collections/SEList.scala | 23 ++++++++++++++++++- .../terminal21/collections/SEListTest.scala | 7 ++++++ .../org/terminal21/client/Controller.scala | 6 ++--- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/terminal21-server-client-common/src/main/scala/org/terminal21/collections/SEList.scala b/terminal21-server-client-common/src/main/scala/org/terminal21/collections/SEList.scala index a9397e5b..fc78afe7 100644 --- a/terminal21-server-client-common/src/main/scala/org/terminal21/collections/SEList.scala +++ b/terminal21-server-client-common/src/main/scala/org/terminal21/collections/SEList.scala @@ -4,13 +4,23 @@ import java.util.concurrent.CountDownLatch class SEList[A]: @volatile private var currentNode: NormalNode[A] = NormalNode(None, EndNode) - def iterator: Iterator[A] = new SEBlockingIterator(currentNode) + /** @return + * A new iterator that only reads elements that are added before the iterator is created. + */ + def iterator: SEBlockingIterator[A] = new SEBlockingIterator(currentNode) + + /** Add a poison pill to terminate all iterators. + */ def poisonPill(): Unit = synchronized: currentNode.valueAndNext = (None, PoisonPillNode) currentNode.latch.countDown() + /** Adds an item that will be visible to all iterators that were created before this item was added. + * @param item + * the item + */ def add(item: A): Unit = val cn = synchronized: val cn = currentNode @@ -22,11 +32,22 @@ class SEList[A]: cn.latch.countDown() class SEBlockingIterator[A](@volatile var currentNode: NormalNode[A]) extends Iterator[A]: + /** @return + * true if hasNext & next() will return immediately with the next value. This won't block. + */ + def isNextAvailable: Boolean = currentNode.hasValue + + /** @return + * true if there is a next() but blocks otherwise till next() becomes available or we are at the end of the iterator. + */ override def hasNext: Boolean = currentNode.waitValue() val v = currentNode.valueAndNext._2 if v == PoisonPillNode then false else true + /** @return + * the next element or blocks until the next element becomes available + */ override def next(): A = if hasNext then val v = currentNode.value diff --git a/terminal21-server-client-common/src/test/scala/org/terminal21/collections/SEListTest.scala b/terminal21-server-client-common/src/test/scala/org/terminal21/collections/SEListTest.scala index a37c6f94..85236f71 100644 --- a/terminal21-server-client-common/src/test/scala/org/terminal21/collections/SEListTest.scala +++ b/terminal21-server-client-common/src/test/scala/org/terminal21/collections/SEListTest.scala @@ -67,6 +67,13 @@ class SEListTest extends AnyFunSuiteLike: it.hasNext should be(false) an[NoSuchElementException] should be thrownBy (it.next()) + test("it.isNextAvailable"): + val l = SEList[Int]() + val it = l.iterator + it.isNextAvailable should be(false) + l.add(1) + it.isNextAvailable should be(true) + test("multiple iterators and multi threading"): val l = SEList[Int]() val iterators = for _ <- 1 to 1000 yield diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 85c51f10..e19ef839 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -59,7 +59,9 @@ class Controller[M]( changeBooleanHandlers ++ elements.map(e => e.key -> handler) ) - def iterator: Iterator[M] = + def iterator: Iterator[M] = handledIterator.takeWhile(!_.shouldTerminate).map(_.model) + + def handledIterator: Iterator[HandledEvent[M]] = eventIteratorFactory .takeWhile(!_.isSessionClose) .scanLeft(HandledEvent(initialModel, Nil, false)): (oldHandled, event) => @@ -94,8 +96,6 @@ class Controller[M]( .flatMap: h => // trick to make sure we take the last state of the model when shouldTerminate=true if h.shouldTerminate then Seq(h.copy(shouldTerminate = false), h) else Seq(h) - .takeWhile(!_.shouldTerminate) - .map(_.model) def lastModelOption: Option[M] = iterator.toList.lastOption From 5d91a0c06961747cf4313eb79e0fef25cb0c0f8a Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 20 Feb 2024 20:15:18 +0000 Subject: [PATCH 095/313] - --- .../src/test/scala/tests/LoginFormTest.scala | 21 ++++++++++++++++--- .../org/terminal21/client/Controller.scala | 6 +++--- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala b/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala index 49a788c6..0d3c1902 100644 --- a/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala +++ b/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala @@ -1,10 +1,9 @@ package tests import org.scalatest.funsuite.AnyFunSuiteLike -import org.terminal21.client.{ConnectedSession, ConnectedSessionMock, Controller} import org.scalatest.matchers.should.Matchers.* -import org.terminal21.client.model.* import org.terminal21.client.components.* +import org.terminal21.client.{ConnectedSession, ConnectedSessionMock} import org.terminal21.model.CommandEvent class LoginFormTest extends AnyFunSuiteLike: @@ -27,11 +26,27 @@ class LoginFormTest extends AnyFunSuiteLike: session.fireEvents( CommandEvent.onChange(form.emailInput, "an@email.com"), CommandEvent.onChange(form.passwordInput, "secret"), - CommandEvent.onClick(form.submitButton) + CommandEvent.onClick(form.submitButton), + CommandEvent.sessionClosed ) eventsIt.toList.lastOption should be(Some(Login("an@email.com", "secret"))) + test("user submits invalid email"): + new App: + form.components.render() + val eventsIt = form.controller.handledIterator // get the iterator before we fire the events, otherwise the iterator will be empty + session.fireEvents( + CommandEvent.onChange(form.emailInput, "anemail.com"), + CommandEvent.onClick(form.submitButton), + CommandEvent.sessionClosed + ) + val allHandled = eventsIt.toList + // the form shouldn't have terminated because of the email error + allHandled.exists(_.shouldTerminate) should be(false) + // the email right addon should have rendered with the notOkIcon + allHandled.flatMap(_.renderChanges) should be(Seq(form.emailRightAddon.withChildren(form.notOkIcon))) + class App: given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock val form = new LoginForm(using session) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index e19ef839..8d3a72eb 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -67,14 +67,14 @@ class Controller[M]( .scanLeft(HandledEvent(initialModel, Nil, false)): (oldHandled, event) => val h = eventHandlers.foldLeft(oldHandled): (h, f) => event match - case UiEvent(OnClick(_), receivedBy) => + case UiEvent(OnClick(_), receivedBy) => f(ControllerClickEvent(receivedBy, h.model)) - case UiEvent(OnChange(key, value), receivedBy) => + case UiEvent(OnChange(_, value), receivedBy) => val e = receivedBy match case _: OnChangeEventHandler.CanHandleOnChangeEvent[_] => ControllerChangeEvent(receivedBy, h.model, value) case _: OnChangeBooleanEventHandler.CanHandleOnChangeEvent[_] => ControllerChangeBooleanEvent(receivedBy, h.model, value.toBoolean) f(e) - case x => throw new IllegalStateException(s"Unexpected state $x") + case x => throw new IllegalStateException(s"Unexpected state $x") val handled = event match case UiEvent(OnClick(key), receivedBy) if clickHandlers.contains(key) => From 06b10330dbd617825fd95e13252aa8c60aed2794 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 20 Feb 2024 20:16:28 +0000 Subject: [PATCH 096/313] - --- end-to-end-tests/src/test/scala/tests/LoginFormTest.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala b/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala index 0d3c1902..015dbacf 100644 --- a/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala +++ b/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala @@ -35,7 +35,7 @@ class LoginFormTest extends AnyFunSuiteLike: test("user submits invalid email"): new App: form.components.render() - val eventsIt = form.controller.handledIterator // get the iterator before we fire the events, otherwise the iterator will be empty + val eventsIt = form.controller.handledIterator // get the iterator that iterates Handled instances so that we can assert on renderChanges session.fireEvents( CommandEvent.onChange(form.emailInput, "anemail.com"), CommandEvent.onClick(form.submitButton), From 974fd4f362a3dd7d313bf135293ae811ad565dd9 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 20 Feb 2024 20:17:06 +0000 Subject: [PATCH 097/313] - --- end-to-end-tests/src/test/scala/tests/LoginFormTest.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala b/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala index 015dbacf..89eb9515 100644 --- a/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala +++ b/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala @@ -27,7 +27,7 @@ class LoginFormTest extends AnyFunSuiteLike: CommandEvent.onChange(form.emailInput, "an@email.com"), CommandEvent.onChange(form.passwordInput, "secret"), CommandEvent.onClick(form.submitButton), - CommandEvent.sessionClosed + CommandEvent.sessionClosed // every test should close the session so that the iterator doesn't block if converted to a list. ) eventsIt.toList.lastOption should be(Some(Login("an@email.com", "secret"))) From d504e1a3d445d5f296fc1a367f76a664ffa05bc5 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 20 Feb 2024 20:56:35 +0000 Subject: [PATCH 098/313] - --- .../org/terminal21/client/Controller.scala | 23 +++++++++++++------ .../terminal21/client/ControllerTest.scala | 11 +++++++++ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 8d3a72eb..8364fa74 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -64,7 +64,7 @@ class Controller[M]( def handledIterator: Iterator[HandledEvent[M]] = eventIteratorFactory .takeWhile(!_.isSessionClose) - .scanLeft(HandledEvent(initialModel, Nil, false)): (oldHandled, event) => + .scanLeft(HandledEvent(initialModel, Nil, Nil, false)): (oldHandled, event) => val h = eventHandlers.foldLeft(oldHandled): (h, f) => event match case UiEvent(OnClick(_), receivedBy) => @@ -93,6 +93,10 @@ class Controller[M]( handled .tapEach: handled => renderChanges(handled.renderChanges) + for trc <- handled.timedRenderChanges do + fiberExecutor.submit: + Thread.sleep(trc.waitInMs) + renderChanges(trc.renderChanges) .flatMap: h => // trick to make sure we take the last state of the model when shouldTerminate=true if h.shouldTerminate then Seq(h.copy(shouldTerminate = false), h) else Seq(h) @@ -105,14 +109,19 @@ object Controller: trait ControllerEvent[M]: def model: M - def handled: HandledEvent[M] = HandledEvent(model, Nil, false) + def handled: HandledEvent[M] = HandledEvent(model, Nil, Nil, false) case class ControllerClickEvent[M](clicked: UiElement, model: M) extends ControllerEvent[M] case class ControllerChangeEvent[M](changed: UiElement, model: M, newValue: String) extends ControllerEvent[M] case class ControllerChangeBooleanEvent[M](changed: UiElement, model: M, newValue: Boolean) extends ControllerEvent[M] -case class HandledEvent[M](model: M, renderChanges: Seq[UiElement], shouldTerminate: Boolean): - def terminate: HandledEvent[M] = copy(shouldTerminate = true) - def withShouldTerminate(t: Boolean): HandledEvent[M] = copy(shouldTerminate = t) - def withModel(m: M): HandledEvent[M] = copy(model = m) - def withRenderChanges(changed: UiElement*): HandledEvent[M] = copy(renderChanges = changed) +case class HandledEvent[M](model: M, renderChanges: Seq[UiElement], timedRenderChanges: Seq[TimedRenderChanges], shouldTerminate: Boolean): + def terminate: HandledEvent[M] = copy(shouldTerminate = true) + def withShouldTerminate(t: Boolean): HandledEvent[M] = copy(shouldTerminate = t) + def withModel(m: M): HandledEvent[M] = copy(model = m) + def withRenderChanges(changed: UiElement*): HandledEvent[M] = copy(renderChanges = changed) + def withTimedRenderChanges(changed: TimedRenderChanges*): HandledEvent[M] = copy(timedRenderChanges = changed) + +case class TimedRenderChanges(waitInMs: Long, renderChanges: Seq[UiElement]) +object TimedRenderChanges: + def apply(waitInMs: Long, renderChanges: UiElement): TimedRenderChanges = TimedRenderChanges(waitInMs, Seq(renderChanges)) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index b737f5cc..1a8162f0 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -81,3 +81,14 @@ class ControllerTest extends AnyFunSuiteLike: .toList should be(List(0, 1)) rendered should be(Seq(button.withText("changed"))) + + test("timed changes are rendered"): + @volatile var rendered = Seq.empty[UiElement] + def renderer(s: Seq[UiElement]): Unit = rendered = s + newController(0, Iterator(buttonClick), renderer) + .onEvent: event => + event.handled.withModel(event.model + 1).withTimedRenderChanges(TimedRenderChanges(10, button.withText("changed"))).terminate + .iterator + .toList should be(List(0, 1)) + Thread.sleep(15) + rendered should be(Seq(button.withText("changed"))) From df2b3e4322f08d7573ef60102a2af9fe037ea506 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 20 Feb 2024 21:07:35 +0000 Subject: [PATCH 099/313] - --- .../src/main/scala/tests/LoginForm.scala | 13 ++++++++++--- .../scala/org/terminal21/client/Controller.scala | 12 +++++++----- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/LoginForm.scala b/end-to-end-tests/src/main/scala/tests/LoginForm.scala index 7d8f7b70..48f4b68a 100644 --- a/end-to-end-tests/src/main/scala/tests/LoginForm.scala +++ b/end-to-end-tests/src/main/scala/tests/LoginForm.scala @@ -1,8 +1,9 @@ package tests -import org.terminal21.client.{ConnectedSession, Controller, Sessions} import org.terminal21.client.components.* import org.terminal21.client.components.chakra.* +import org.terminal21.client.components.std.Paragraph +import org.terminal21.client.{ConnectedSession, Controller, Sessions} @main def loginFormApp(): Unit = Sessions @@ -22,6 +23,7 @@ class LoginForm(using session: ConnectedSession): val emailInput = Input(`type` = "email", defaultValue = initialModel.email) val submitButton = Button(text = "Submit") val passwordInput = Input(`type` = "password", defaultValue = initialModel.pwd) + val errorsBox = Box() def run(): Option[Login] = components.render() @@ -44,7 +46,8 @@ class LoginForm(using session: ConnectedSession): InputLeftAddon().withChildren(ViewOffIcon()), passwordInput ), - submitButton + submitButton, + errorsBox ) def controller: Controller[Login] = Controller(initialModel) @@ -52,7 +55,11 @@ class LoginForm(using session: ConnectedSession): val newModel = event.model.copy(email = emailInput.current.value, pwd = passwordInput.current.value) event.handled.withModel(newModel) .onClick(submitButton): clickEvent => - clickEvent.handled.withShouldTerminate(clickEvent.model.isValidEmail) + // if the email is invalid, we will not terminate. We also will render an error that will be visible for 2 seconds + val isValidEmail = clickEvent.model.isValidEmail + val messageBox = + if isValidEmail then errorsBox.current else errorsBox.current.addChildren(Paragraph(text = "Invalid Email", style = Map("color" -> "red"))) + clickEvent.handled.withShouldTerminate(isValidEmail).withRenderChanges(messageBox).addTimedRenderChange(2000, errorsBox) .onChange(emailInput): changeEvent => changeEvent.handled.withRenderChanges(validate(changeEvent.model)) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 8364fa74..25556ce2 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -116,11 +116,13 @@ case class ControllerChangeEvent[M](changed: UiElement, model: M, newValue: Stri case class ControllerChangeBooleanEvent[M](changed: UiElement, model: M, newValue: Boolean) extends ControllerEvent[M] case class HandledEvent[M](model: M, renderChanges: Seq[UiElement], timedRenderChanges: Seq[TimedRenderChanges], shouldTerminate: Boolean): - def terminate: HandledEvent[M] = copy(shouldTerminate = true) - def withShouldTerminate(t: Boolean): HandledEvent[M] = copy(shouldTerminate = t) - def withModel(m: M): HandledEvent[M] = copy(model = m) - def withRenderChanges(changed: UiElement*): HandledEvent[M] = copy(renderChanges = changed) - def withTimedRenderChanges(changed: TimedRenderChanges*): HandledEvent[M] = copy(timedRenderChanges = changed) + def terminate: HandledEvent[M] = copy(shouldTerminate = true) + def withShouldTerminate(t: Boolean): HandledEvent[M] = copy(shouldTerminate = t) + def withModel(m: M): HandledEvent[M] = copy(model = m) + def withRenderChanges(changed: UiElement*): HandledEvent[M] = copy(renderChanges = changed) + def withTimedRenderChanges(changed: TimedRenderChanges*): HandledEvent[M] = copy(timedRenderChanges = changed) + def addTimedRenderChange(waitInMs: Long, renderChanges: UiElement): HandledEvent[M] = + copy(timedRenderChanges = timedRenderChanges :+ TimedRenderChanges(waitInMs, renderChanges)) case class TimedRenderChanges(waitInMs: Long, renderChanges: Seq[UiElement]) object TimedRenderChanges: From 14a4628fb09b947e33b6e2ac22707344e769507e Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 20 Feb 2024 21:09:45 +0000 Subject: [PATCH 100/313] - --- end-to-end-tests/src/main/scala/tests/LoginForm.scala | 3 ++- end-to-end-tests/src/test/scala/tests/LoginFormTest.scala | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/LoginForm.scala b/end-to-end-tests/src/main/scala/tests/LoginForm.scala index 48f4b68a..4157b3aa 100644 --- a/end-to-end-tests/src/main/scala/tests/LoginForm.scala +++ b/end-to-end-tests/src/main/scala/tests/LoginForm.scala @@ -24,6 +24,7 @@ class LoginForm(using session: ConnectedSession): val submitButton = Button(text = "Submit") val passwordInput = Input(`type` = "password", defaultValue = initialModel.pwd) val errorsBox = Box() + val errorMsgInvalidEmail = Paragraph(text = "Invalid Email", style = Map("color" -> "red")) def run(): Option[Login] = components.render() @@ -58,7 +59,7 @@ class LoginForm(using session: ConnectedSession): // if the email is invalid, we will not terminate. We also will render an error that will be visible for 2 seconds val isValidEmail = clickEvent.model.isValidEmail val messageBox = - if isValidEmail then errorsBox.current else errorsBox.current.addChildren(Paragraph(text = "Invalid Email", style = Map("color" -> "red"))) + if isValidEmail then errorsBox.current else errorsBox.current.addChildren(errorMsgInvalidEmail) clickEvent.handled.withShouldTerminate(isValidEmail).withRenderChanges(messageBox).addTimedRenderChange(2000, errorsBox) .onChange(emailInput): changeEvent => changeEvent.handled.withRenderChanges(validate(changeEvent.model)) diff --git a/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala b/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala index 89eb9515..bf14c3b6 100644 --- a/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala +++ b/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala @@ -45,7 +45,9 @@ class LoginFormTest extends AnyFunSuiteLike: // the form shouldn't have terminated because of the email error allHandled.exists(_.shouldTerminate) should be(false) // the email right addon should have rendered with the notOkIcon - allHandled.flatMap(_.renderChanges) should be(Seq(form.emailRightAddon.withChildren(form.notOkIcon))) + allHandled.flatMap(_.renderChanges) should be( + Seq(form.emailRightAddon.withChildren(form.notOkIcon), form.errorsBox.withChildren(form.errorMsgInvalidEmail)) + ) class App: given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock From d4a031a994c51fc163c67bf0b90d4d7cc316b16e Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 21 Feb 2024 11:52:18 +0000 Subject: [PATCH 101/313] - --- .../src/main/scala/tests/LoginForm.scala | 2 +- .../src/test/scala/tests/LoginFormTest.scala | 29 +++--- .../org/terminal21/client/Controller.scala | 89 ++++++++++--------- .../client/collections/EventIterator.scala | 13 +++ .../terminal21/client/ControllerTest.scala | 18 ++-- .../collections/EventIteratorTest.scala | 17 ++++ 6 files changed, 101 insertions(+), 67 deletions(-) create mode 100644 terminal21-ui-std/src/main/scala/org/terminal21/client/collections/EventIterator.scala create mode 100644 terminal21-ui-std/src/test/scala/org/terminal21/client/collections/EventIteratorTest.scala diff --git a/end-to-end-tests/src/main/scala/tests/LoginForm.scala b/end-to-end-tests/src/main/scala/tests/LoginForm.scala index 4157b3aa..319928b8 100644 --- a/end-to-end-tests/src/main/scala/tests/LoginForm.scala +++ b/end-to-end-tests/src/main/scala/tests/LoginForm.scala @@ -28,7 +28,7 @@ class LoginForm(using session: ConnectedSession): def run(): Option[Login] = components.render() - controller.lastModelOption + controller.eventsIterator.lastOption def components: Seq[UiElement] = Seq( diff --git a/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala b/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala index bf14c3b6..66d1f9da 100644 --- a/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala +++ b/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala @@ -7,6 +7,12 @@ import org.terminal21.client.{ConnectedSession, ConnectedSessionMock} import org.terminal21.model.CommandEvent class LoginFormTest extends AnyFunSuiteLike: + + class App: + given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock + val form = new LoginForm(using session) + def allComponents = form.components.flatMap(_.flat) + test("renders email input"): new App: allComponents should contain(form.emailInput) @@ -22,7 +28,7 @@ class LoginFormTest extends AnyFunSuiteLike: test("user submits validated data"): new App: form.components.render() - val eventsIt = form.controller.iterator // get the iterator before we fire the events, otherwise the iterator will be empty + val eventsIt = form.controller.eventsIterator // get the iterator before we fire the events, otherwise the iterator will be empty session.fireEvents( CommandEvent.onChange(form.emailInput, "an@email.com"), CommandEvent.onChange(form.passwordInput, "secret"), @@ -30,26 +36,21 @@ class LoginFormTest extends AnyFunSuiteLike: CommandEvent.sessionClosed // every test should close the session so that the iterator doesn't block if converted to a list. ) - eventsIt.toList.lastOption should be(Some(Login("an@email.com", "secret"))) + eventsIt.lastOption should be(Some(Login("an@email.com", "secret"))) test("user submits invalid email"): new App: form.components.render() - val eventsIt = form.controller.handledIterator // get the iterator that iterates Handled instances so that we can assert on renderChanges + val eventsIt = form.controller.handledEventsIterator // get the iterator that iterates Handled instances so that we can assert on renderChanges session.fireEvents( - CommandEvent.onChange(form.emailInput, "anemail.com"), + CommandEvent.onChange(form.emailInput, "invalid-email.com"), CommandEvent.onClick(form.submitButton), CommandEvent.sessionClosed ) val allHandled = eventsIt.toList - // the form shouldn't have terminated because of the email error + // the event processing shouldn't have terminated because of the email error allHandled.exists(_.shouldTerminate) should be(false) - // the email right addon should have rendered with the notOkIcon - allHandled.flatMap(_.renderChanges) should be( - Seq(form.emailRightAddon.withChildren(form.notOkIcon), form.errorsBox.withChildren(form.errorMsgInvalidEmail)) - ) - - class App: - given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock - val form = new LoginForm(using session) - def allComponents = form.components.flatMap(_.flat) + // the email right addon should have rendered with the notOkIcon when the user typed the incorrect email + allHandled(1).renderChanges should be(Seq(form.emailRightAddon.withChildren(form.notOkIcon))) + // An error message in the errorsBox should be displayed when the user clicked on the submit + allHandled(2).renderChanges should be(Seq(form.errorsBox.withChildren(form.errorMsgInvalidEmail))) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 25556ce2..fa182a43 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -2,6 +2,7 @@ package org.terminal21.client import org.terminal21.client.OnChangeEventHandler.CanHandleOnChangeEvent import org.terminal21.client.OnClickEventHandler.CanHandleOnClickEvent +import org.terminal21.client.collections.EventIterator import org.terminal21.client.components.UiElement import org.terminal21.client.model.{GlobalEvent, UiEvent} import org.terminal21.model.{OnChange, OnClick} @@ -43,12 +44,14 @@ class Controller[M]( changeBooleanHandlers ) - def onChange(element: UiElement & OnChangeBooleanEventHandler.CanHandleOnChangeEvent[_])(handler: ControllerChangeBooleanEvent[M] => HandledEvent[M]) = + def onChange(element: UiElement & OnChangeBooleanEventHandler.CanHandleOnChangeEvent[_])( + handler: ControllerChangeBooleanEvent[M] => HandledEvent[M] + ): Controller[M] = onChangedBoolean(element)(handler) def onChangedBoolean( elements: UiElement & OnChangeBooleanEventHandler.CanHandleOnChangeEvent[_]* - )(handler: ControllerChangeBooleanEvent[M] => HandledEvent[M]) = + )(handler: ControllerChangeBooleanEvent[M] => HandledEvent[M]): Controller[M] = new Controller( eventIteratorFactory, renderChanges, @@ -59,49 +62,49 @@ class Controller[M]( changeBooleanHandlers ++ elements.map(e => e.key -> handler) ) - def iterator: Iterator[M] = handledIterator.takeWhile(!_.shouldTerminate).map(_.model) + def eventsIterator: EventIterator[M] = new EventIterator(handledEventsIterator.takeWhile(!_.shouldTerminate).map(_.model)) - def handledIterator: Iterator[HandledEvent[M]] = - eventIteratorFactory - .takeWhile(!_.isSessionClose) - .scanLeft(HandledEvent(initialModel, Nil, Nil, false)): (oldHandled, event) => - val h = eventHandlers.foldLeft(oldHandled): (h, f) => - event match - case UiEvent(OnClick(_), receivedBy) => - f(ControllerClickEvent(receivedBy, h.model)) - case UiEvent(OnChange(_, value), receivedBy) => - val e = receivedBy match - case _: OnChangeEventHandler.CanHandleOnChangeEvent[_] => ControllerChangeEvent(receivedBy, h.model, value) - case _: OnChangeBooleanEventHandler.CanHandleOnChangeEvent[_] => ControllerChangeBooleanEvent(receivedBy, h.model, value.toBoolean) - f(e) - case x => throw new IllegalStateException(s"Unexpected state $x") + def handledEventsIterator: EventIterator[HandledEvent[M]] = + new EventIterator( + eventIteratorFactory + .takeWhile(!_.isSessionClose) + .scanLeft(HandledEvent(initialModel, Nil, Nil, false)): (oldHandled, event) => + val h = eventHandlers.foldLeft(oldHandled): (h, f) => + event match + case UiEvent(OnClick(_), receivedBy) => + f(ControllerClickEvent(receivedBy, h.model)) + case UiEvent(OnChange(_, value), receivedBy) => + val e = receivedBy match + case _: OnChangeEventHandler.CanHandleOnChangeEvent[_] => ControllerChangeEvent(receivedBy, h.model, value) + case _: OnChangeBooleanEventHandler.CanHandleOnChangeEvent[_] => ControllerChangeBooleanEvent(receivedBy, h.model, value.toBoolean) + f(e) + case x => throw new IllegalStateException(s"Unexpected state $x") - val handled = event match - case UiEvent(OnClick(key), receivedBy) if clickHandlers.contains(key) => - val handler = clickHandlers(key) - val handled = handler(ControllerClickEvent(receivedBy, h.model)) - handled - case UiEvent(OnChange(key, value), receivedBy) if changeHandlers.contains(key) => - val handler = changeHandlers(key) - val handled = handler(ControllerChangeEvent(receivedBy, h.model, value)) - handled - case UiEvent(OnChange(key, value), receivedBy) if changeBooleanHandlers.contains(key) => - val handler = changeBooleanHandlers(key) - val handled = handler(ControllerChangeBooleanEvent(receivedBy, h.model, value.toBoolean)) - handled - case _ => h - handled - .tapEach: handled => - renderChanges(handled.renderChanges) - for trc <- handled.timedRenderChanges do - fiberExecutor.submit: - Thread.sleep(trc.waitInMs) - renderChanges(trc.renderChanges) - .flatMap: h => - // trick to make sure we take the last state of the model when shouldTerminate=true - if h.shouldTerminate then Seq(h.copy(shouldTerminate = false), h) else Seq(h) - - def lastModelOption: Option[M] = iterator.toList.lastOption + val handled = event match + case UiEvent(OnClick(key), receivedBy) if clickHandlers.contains(key) => + val handler = clickHandlers(key) + val handled = handler(ControllerClickEvent(receivedBy, h.model)) + handled + case UiEvent(OnChange(key, value), receivedBy) if changeHandlers.contains(key) => + val handler = changeHandlers(key) + val handled = handler(ControllerChangeEvent(receivedBy, h.model, value)) + handled + case UiEvent(OnChange(key, value), receivedBy) if changeBooleanHandlers.contains(key) => + val handler = changeBooleanHandlers(key) + val handled = handler(ControllerChangeBooleanEvent(receivedBy, h.model, value.toBoolean)) + handled + case _ => h + handled + .tapEach: handled => + renderChanges(handled.renderChanges) + for trc <- handled.timedRenderChanges do + fiberExecutor.submit: + Thread.sleep(trc.waitInMs) + renderChanges(trc.renderChanges) + .flatMap: h => + // trick to make sure we take the last state of the model when shouldTerminate=true + if h.shouldTerminate then Seq(h.copy(shouldTerminate = false), h) else Seq(h) + ) object Controller: def apply[M](initialModel: M)(using session: ConnectedSession): Controller[M] = diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/collections/EventIterator.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/collections/EventIterator.scala new file mode 100644 index 00000000..2a14248a --- /dev/null +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/collections/EventIterator.scala @@ -0,0 +1,13 @@ +package org.terminal21.client.collections + +class EventIterator[A](it: Iterator[A]) extends Iterator[A]: + override def hasNext: Boolean = it.hasNext + override def next(): A = it.next() + + def lastOption: Option[A] = + var last = Option.empty[A] + while hasNext do last = Some(next()) + last + +object EventIterator: + def apply[A](items: A*): EventIterator[A] = new EventIterator(Iterator(items*)) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 1a8162f0..06baac27 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -23,7 +23,7 @@ class ControllerTest extends AnyFunSuiteLike: newController(0, Iterator(buttonClick)) .onEvent: event => if event.model > 1 then event.handled.terminate else event.handled.withModel(event.model + 1) - .iterator + .eventsIterator .toList should be(List(0, 1)) test("onEvent is called for change"): @@ -31,7 +31,7 @@ class ControllerTest extends AnyFunSuiteLike: .onEvent: case event @ ControllerChangeEvent(`input`, 0, "new-value") => if event.model > 1 then event.handled.terminate else event.handled.withModel(event.model + 1) - .iterator + .eventsIterator .toList should be(List(0, 1)) test("onEvent is called for change/boolean"): @@ -39,35 +39,35 @@ class ControllerTest extends AnyFunSuiteLike: .onEvent: case event @ ControllerChangeBooleanEvent(`checkbox`, 0, true) => if event.model > 1 then event.handled.terminate else event.handled.withModel(event.model + 1) - .iterator + .eventsIterator .toList should be(List(0, 1)) test("onClick is called"): newController(0, Iterator(buttonClick)) .onClick(button): event => event.handled.withModel(100).terminate - .iterator + .eventsIterator .toList should be(List(0, 100)) test("onChange is called"): newController(0, Iterator(inputChange)) .onChange(input): event => event.handled.withModel(100).terminate - .iterator + .eventsIterator .toList should be(List(0, 100)) test("onChange/boolean is called"): newController(0, Iterator(checkBoxChange)) .onChange(checkbox): event => event.handled.withModel(100).terminate - .iterator + .eventsIterator .toList should be(List(0, 100)) test("terminate is obeyed and latest model state is iterated"): newController(0, Iterator(buttonClick, buttonClick, buttonClick)) .onEvent: event => if event.model > 1 then event.handled.terminate.withModel(100) else event.handled.withModel(event.model + 1) - .iterator + .eventsIterator .toList should be(List(0, 1, 2, 100)) test("changes are rendered"): @@ -77,7 +77,7 @@ class ControllerTest extends AnyFunSuiteLike: newController(0, Iterator(buttonClick), renderer) .onEvent: event => event.handled.withModel(event.model + 1).withRenderChanges(button.withText("changed")).terminate - .iterator + .eventsIterator .toList should be(List(0, 1)) rendered should be(Seq(button.withText("changed"))) @@ -88,7 +88,7 @@ class ControllerTest extends AnyFunSuiteLike: newController(0, Iterator(buttonClick), renderer) .onEvent: event => event.handled.withModel(event.model + 1).withTimedRenderChanges(TimedRenderChanges(10, button.withText("changed"))).terminate - .iterator + .eventsIterator .toList should be(List(0, 1)) Thread.sleep(15) rendered should be(Seq(button.withText("changed"))) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/collections/EventIteratorTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/collections/EventIteratorTest.scala new file mode 100644 index 00000000..e82021ad --- /dev/null +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/collections/EventIteratorTest.scala @@ -0,0 +1,17 @@ +package org.terminal21.client.collections + +import org.scalatest.funsuite.AnyFunSuiteLike +import org.scalatest.matchers.should.Matchers.* + +class EventIteratorTest extends AnyFunSuiteLike: + test("works as normal iterator"): + EventIterator(1, 2, 3).toList should be(List(1, 2, 3)) + + test("works as normal iterator when empty"): + EventIterator().toList should be(Nil) + + test("lastOption when available"): + EventIterator(1, 2, 3).lastOption should be(Some(3)) + + test("lastOption when not available"): + EventIterator().lastOption should be(None) From 2d460d6d88f1c048d9a8bc167ba6be134af3e37d Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 21 Feb 2024 12:21:13 +0000 Subject: [PATCH 102/313] - --- .../src/main/scala/tests/LoginForm.scala | 41 ++++++++++++++++--- .../terminal21/client/ConnectedSession.scala | 1 - 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/LoginForm.scala b/end-to-end-tests/src/main/scala/tests/LoginForm.scala index 319928b8..8b937e48 100644 --- a/end-to-end-tests/src/main/scala/tests/LoginForm.scala +++ b/end-to-end-tests/src/main/scala/tests/LoginForm.scala @@ -2,7 +2,7 @@ package tests import org.terminal21.client.components.* import org.terminal21.client.components.chakra.* -import org.terminal21.client.components.std.Paragraph +import org.terminal21.client.components.std.{NewLine, Paragraph} import org.terminal21.client.{ConnectedSession, Controller, Sessions} @main def loginFormApp(): Unit = @@ -10,10 +10,12 @@ import org.terminal21.client.{ConnectedSession, Controller, Sessions} .withNewSession("login-form", "Login Form") .connect: session => given ConnectedSession = session - val form = new LoginForm() - form.run() match - case Some(login) if !session.isClosed => println(s"Login will be processed: $login") - case _ => println("Login cancelled") + val confirmed = for + login <- new LoginForm().run() + isYes <- new LoggedIn(login).run() + yield isYes + + if confirmed.getOrElse(false) then println("User confirmed the details") else println("Not confirmed") class LoginForm(using session: ConnectedSession): private val initialModel = Login("my@email.com", "mysecret") @@ -69,3 +71,32 @@ class LoginForm(using session: ConnectedSession): private case class Login(email: String, pwd: String): def isValidEmail: Boolean = email.contains("@") + +class LoggedIn(login: Login)(using session: ConnectedSession): + val yesButton = Button(text = "Yes") + val noButton = Button(text = "No") + + def run(): Option[Boolean] = + session.clear() + components.render() + controller.eventsIterator.lastOption + + def components = Seq( + Paragraph().withChildren( + Text(text = "Are your details correct?"), + NewLine(), + Text(text = s"email : ${login.email}"), + NewLine(), + Text(text = s"password : ${login.pwd}") + ), + HStack().withChildren(yesButton, noButton) + ) + + /** @return + * A controller with a boolean value, true if user clicked "Yes", false for "No" + */ + def controller = Controller(false) + .onClick(yesButton): e => + e.handled.withModel(true).terminate + .onClick(noButton): e => + e.handled.withModel(false).terminate diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index c62d13a4..7a20b6c6 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -26,7 +26,6 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se /** Clears all UI elements and event handlers. Renders a blank UI */ def clear(): Unit = - render() handlers.clear() modifiedElements.clear() removeGlobalEventHandler() From 723d4f918a6d01184645eb639b6bb4a2886af6a2 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 21 Feb 2024 12:38:36 +0000 Subject: [PATCH 103/313] - --- .../src/main/scala/tests/LoginForm.scala | 14 ++++++++------ .../client/collections/EventIterator.scala | 6 ++++++ .../client/collections/EventIteratorTest.scala | 11 +++++++++++ 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/LoginForm.scala b/end-to-end-tests/src/main/scala/tests/LoginForm.scala index 8b937e48..3862d84e 100644 --- a/end-to-end-tests/src/main/scala/tests/LoginForm.scala +++ b/end-to-end-tests/src/main/scala/tests/LoginForm.scala @@ -17,6 +17,11 @@ import org.terminal21.client.{ConnectedSession, Controller, Sessions} if confirmed.getOrElse(false) then println("User confirmed the details") else println("Not confirmed") +private case class Login(email: String, pwd: String): + def isValidEmail: Boolean = email.contains("@") + +/** The login form. Displays an email and password input and a submit button. When run() it will fill in the Login(email,pwd) model. + */ class LoginForm(using session: ConnectedSession): private val initialModel = Login("my@email.com", "mysecret") val okIcon = CheckCircleIcon(color = Some("green")) @@ -30,7 +35,7 @@ class LoginForm(using session: ConnectedSession): def run(): Option[Login] = components.render() - controller.eventsIterator.lastOption + controller.eventsIterator.lastOptionOrNoneIfSessionClosed def components: Seq[UiElement] = Seq( @@ -69,16 +74,13 @@ class LoginForm(using session: ConnectedSession): private def validate(login: Login): InputRightAddon = if login.isValidEmail then emailRightAddon.withChildren(okIcon) else emailRightAddon.withChildren(notOkIcon) -private case class Login(email: String, pwd: String): - def isValidEmail: Boolean = email.contains("@") - class LoggedIn(login: Login)(using session: ConnectedSession): val yesButton = Button(text = "Yes") val noButton = Button(text = "No") def run(): Option[Boolean] = - session.clear() - components.render() + session.clear() // when transitioning to a new UI page, we need to clear previous event handlers, iterators etc. + components.render() // this will clear the UI and render the components for this form controller.eventsIterator.lastOption def components = Seq( diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/collections/EventIterator.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/collections/EventIterator.scala index 2a14248a..f8cf1cae 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/collections/EventIterator.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/collections/EventIterator.scala @@ -1,5 +1,7 @@ package org.terminal21.client.collections +import org.terminal21.client.ConnectedSession + class EventIterator[A](it: Iterator[A]) extends Iterator[A]: override def hasNext: Boolean = it.hasNext override def next(): A = it.next() @@ -9,5 +11,9 @@ class EventIterator[A](it: Iterator[A]) extends Iterator[A]: while hasNext do last = Some(next()) last + def lastOptionOrNoneIfSessionClosed(using session: ConnectedSession) = + val v = lastOption + if session.isClosed then None else v + object EventIterator: def apply[A](items: A*): EventIterator[A] = new EventIterator(Iterator(items*)) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/collections/EventIteratorTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/collections/EventIteratorTest.scala index e82021ad..53c5891f 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/collections/EventIteratorTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/collections/EventIteratorTest.scala @@ -2,6 +2,8 @@ package org.terminal21.client.collections import org.scalatest.funsuite.AnyFunSuiteLike import org.scalatest.matchers.should.Matchers.* +import org.terminal21.client.ConnectedSessionMock +import org.terminal21.model.{CommandEvent, SessionClosed} class EventIteratorTest extends AnyFunSuiteLike: test("works as normal iterator"): @@ -15,3 +17,12 @@ class EventIteratorTest extends AnyFunSuiteLike: test("lastOption when not available"): EventIterator().lastOption should be(None) + + test("lastOptionOrNoneIfSessionClosed when session open"): + val session = ConnectedSessionMock.newConnectedSessionMock + EventIterator(1, 2).lastOptionOrNoneIfSessionClosed(using session) should be(Some(2)) + + test("lastOptionOrNoneIfSessionClosed when session closed"): + val session = ConnectedSessionMock.newConnectedSessionMock + session.fireEvent(CommandEvent.sessionClosed) + EventIterator(1, 2).lastOptionOrNoneIfSessionClosed(using session) should be(None) From 74d2ee490502c42596908d4edac126d218379920 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 21 Feb 2024 13:00:27 +0000 Subject: [PATCH 104/313] - --- .../src/main/scala/tests/LoginForm.scala | 29 ++++++++------- .../src/test/scala/tests/LoggedInTest.scala | 36 +++++++++++++++++++ 2 files changed, 52 insertions(+), 13 deletions(-) create mode 100644 end-to-end-tests/src/test/scala/tests/LoggedInTest.scala diff --git a/end-to-end-tests/src/main/scala/tests/LoginForm.scala b/end-to-end-tests/src/main/scala/tests/LoginForm.scala index 3862d84e..4dc9dde6 100644 --- a/end-to-end-tests/src/main/scala/tests/LoginForm.scala +++ b/end-to-end-tests/src/main/scala/tests/LoginForm.scala @@ -17,7 +17,7 @@ import org.terminal21.client.{ConnectedSession, Controller, Sessions} if confirmed.getOrElse(false) then println("User confirmed the details") else println("Not confirmed") -private case class Login(email: String, pwd: String): +case class Login(email: String, pwd: String): def isValidEmail: Boolean = email.contains("@") /** The login form. Displays an email and password input and a submit button. When run() it will fill in the Login(email,pwd) model. @@ -75,24 +75,27 @@ class LoginForm(using session: ConnectedSession): if login.isValidEmail then emailRightAddon.withChildren(okIcon) else emailRightAddon.withChildren(notOkIcon) class LoggedIn(login: Login)(using session: ConnectedSession): - val yesButton = Button(text = "Yes") - val noButton = Button(text = "No") + val yesButton = Button(text = "Yes") + val noButton = Button(text = "No") + val emailDetails = Text(text = s"email : ${login.email}") + val passwordDetails = Text(text = s"password : ${login.pwd}") def run(): Option[Boolean] = session.clear() // when transitioning to a new UI page, we need to clear previous event handlers, iterators etc. components.render() // this will clear the UI and render the components for this form controller.eventsIterator.lastOption - def components = Seq( - Paragraph().withChildren( - Text(text = "Are your details correct?"), - NewLine(), - Text(text = s"email : ${login.email}"), - NewLine(), - Text(text = s"password : ${login.pwd}") - ), - HStack().withChildren(yesButton, noButton) - ) + def components = + Seq( + Paragraph().withChildren( + Text(text = "Are your details correct?"), + NewLine(), + emailDetails, + NewLine(), + passwordDetails + ), + HStack().withChildren(yesButton, noButton) + ) /** @return * A controller with a boolean value, true if user clicked "Yes", false for "No" diff --git a/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala b/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala new file mode 100644 index 00000000..4ed895ab --- /dev/null +++ b/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala @@ -0,0 +1,36 @@ +package tests + +import org.scalatest.funsuite.AnyFunSuiteLike +import org.terminal21.client.{ConnectedSession, ConnectedSessionMock} +import org.scalatest.matchers.should.Matchers.* +import org.terminal21.client.components.* +import org.terminal21.model.CommandEvent + +class LoggedInTest extends AnyFunSuiteLike: + class App: + val login = Login("my@email.com", "secret") + given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock + val form = new LoggedIn(login) + def allComponents = form.components.flatMap(_.flat) + + test("renders email details"): + new App: + allComponents should contain(form.emailDetails) + + test("renders password details"): + new App: + allComponents should contain(form.passwordDetails) + + test("yes clicked"): + new App: + form.components.render() + val eventsIt = form.controller.eventsIterator + session.fireEvents(CommandEvent.onClick(form.yesButton), CommandEvent.sessionClosed) + eventsIt.lastOption should be(Some(true)) + + test("no clicked"): + new App: + form.components.render() + val eventsIt = form.controller.eventsIterator + session.fireEvents(CommandEvent.onClick(form.noButton), CommandEvent.sessionClosed) + eventsIt.lastOption should be(Some(false)) From b03e0e1bc535c0f3576bca0db030fa2564c5b7fb Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 21 Feb 2024 14:45:37 +0000 Subject: [PATCH 105/313] - --- .../serverapp/bundled/AppManager.scala | 61 ++++++++++--------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala index 773ea3c9..9c3321cf 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala @@ -17,38 +17,43 @@ class AppManager(serverSideSessions: ServerSideSessions, fiberExecutor: FiberExe .andOptions(SessionOptions(alwaysOpen = true)) .connect: session => given ConnectedSession = session + new AppManagerPage(apps, startApp).run() - val appRows = apps.map: app => - val link = Link(text = app.name).onClick: () => - startApp(app) - Seq[UiElement](link, Text(text = app.description)) - val appsTable = QuickTable( - caption = Some("Apps installed on the server, click one to run it."), - rows = appRows - ).withHeaders("App Name", "Description") + private def startApp(app: ServerSideApp): Unit = + app.createSession(serverSideSessions, dependencies) - Seq( - Header1(text = "Terminal 21 Manager"), - Paragraph( - text = """ - |Here you can run all the installed apps on the server.""".stripMargin - ), - appsTable, - Paragraph().withChildren( - Span(text = "Have a question? Please ask at "), - Link( - text = "terminal21's discussion board ", - href = "https://github.com/kostaskougios/terminal21-restapi/discussions", - color = Some("teal.500"), - isExternal = Some(true) - ).withChildren(ExternalLinkIcon(mx = Some("2px"))) - ) - ).render() +class AppManagerPage(apps: Seq[ServerSideApp], startApp: ServerSideApp => Unit)(using session: ConnectedSession): + def run(): Unit = + components.render() + session.waitTillUserClosesSession() - session.waitTillUserClosesSession() + def components = + val appRows = apps.map: app => + val link = Link(text = app.name).onClick: () => + startApp(app) + Seq[UiElement](link, Text(text = app.description)) + val appsTable = QuickTable( + caption = Some("Apps installed on the server, click one to run it."), + rows = appRows + ).withHeaders("App Name", "Description") - private def startApp(app: ServerSideApp): Unit = - app.createSession(serverSideSessions, dependencies) + Seq( + Header1(text = "Terminal 21 Manager"), + Paragraph( + text = """ + |Here you can run all the installed apps on the server.""".stripMargin + ), + appsTable, + Paragraph().withChildren( + Span(text = "Have a question? Please ask at "), + Link( + text = "terminal21's discussion board ", + href = "https://github.com/kostaskougios/terminal21-restapi/discussions", + color = Some("teal.500"), + isExternal = Some(true) + ).withChildren(ExternalLinkIcon(mx = Some("2px"))) + ) + ) trait AppManagerBeans: def serverSideSessions: ServerSideSessions From 019676309f5ca5469c97dfc38c940d780dd76df6 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 21 Feb 2024 15:54:45 +0000 Subject: [PATCH 106/313] - --- .../serverapp/bundled/AppManager.scala | 29 ++++++++++++++----- .../org/terminal21/client/Controller.scala | 7 ++++- .../org/terminal21/client/EventHandler.scala | 5 +++- .../client/collections/EventIterator.scala | 4 ++- .../client/model/OnClickController.scala | 10 +++++++ 5 files changed, 44 insertions(+), 11 deletions(-) create mode 100644 terminal21-ui-std/src/main/scala/org/terminal21/client/model/OnClickController.scala diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala index 9c3321cf..46faf339 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala @@ -1,7 +1,7 @@ package org.terminal21.serverapp.bundled import functions.fibers.FiberExecutor -import org.terminal21.client.ConnectedSession +import org.terminal21.client.{ConnectedSession, Controller} import org.terminal21.client.components.* import org.terminal21.client.components.chakra.* import org.terminal21.client.components.std.{Header1, Paragraph, Span} @@ -20,21 +20,27 @@ class AppManager(serverSideSessions: ServerSideSessions, fiberExecutor: FiberExe new AppManagerPage(apps, startApp).run() private def startApp(app: ServerSideApp): Unit = - app.createSession(serverSideSessions, dependencies) + fiberExecutor.submit: + app.createSession(serverSideSessions, dependencies) class AppManagerPage(apps: Seq[ServerSideApp], startApp: ServerSideApp => Unit)(using session: ConnectedSession): def run(): Unit = components.render() - session.waitTillUserClosesSession() + controller.eventsIterator + .tapEach: m => + for app <- m.startApp do startApp(app) + .foreach(_ => ()) + + case class AppRow(app: ServerSideApp, link: Link, text: Text): + def row: Seq[UiElement] = Seq(link, text) + + val appRows = apps.map: app => + AppRow(app, Link(text = app.name), Text(text = app.description)) def components = - val appRows = apps.map: app => - val link = Link(text = app.name).onClick: () => - startApp(app) - Seq[UiElement](link, Text(text = app.description)) val appsTable = QuickTable( caption = Some("Apps installed on the server, click one to run it."), - rows = appRows + rows = appRows.map(_.row) ).withHeaders("App Name", "Description") Seq( @@ -55,6 +61,13 @@ class AppManagerPage(apps: Seq[ServerSideApp], startApp: ServerSideApp => Unit)( ) ) + case class Model(startApp: Option[ServerSideApp]) + def controller: Controller[Model] = + val clickControllers = appRows.map: appRow => + appRow.link.onClickController[Model]: event => + event.handled.withModel(event.model.copy(startApp = Some(appRow.app))) + Controller(Model(None)).onClick(clickControllers) + trait AppManagerBeans: def serverSideSessions: ServerSideSessions def fiberExecutor: FiberExecutor diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index fa182a43..9e3f0de2 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -4,7 +4,7 @@ import org.terminal21.client.OnChangeEventHandler.CanHandleOnChangeEvent import org.terminal21.client.OnClickEventHandler.CanHandleOnClickEvent import org.terminal21.client.collections.EventIterator import org.terminal21.client.components.UiElement -import org.terminal21.client.model.{GlobalEvent, UiEvent} +import org.terminal21.client.model.{GlobalEvent, OnClickController, UiEvent} import org.terminal21.model.{OnChange, OnClick} class Controller[M]( @@ -19,6 +19,11 @@ class Controller[M]( def onEvent(handler: ControllerEvent[M] => HandledEvent[M]) = new Controller(eventIteratorFactory, renderChanges, initialModel, eventHandlers :+ handler, clickHandlers, changeHandlers, changeBooleanHandlers) + def onClick(controller: OnClickController[M]): Controller[M] = onClick(controller.element)(controller.handler) + def onClick(controllers: Seq[OnClickController[M]]): Controller[M] = + controllers.foldLeft(this): (c, h) => + c.onClick(h.element)(h.handler) + def onClick(element: UiElement & CanHandleOnClickEvent[_])(handler: ControllerClickEvent[M] => HandledEvent[M]): Controller[M] = onClicked(element)(handler) def onClicked(elements: UiElement & CanHandleOnClickEvent[_]*)(handler: ControllerClickEvent[M] => HandledEvent[M]): Controller[M] = new Controller( diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala index b2644e89..693846e2 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala @@ -1,7 +1,7 @@ package org.terminal21.client import org.terminal21.client.components.UiElement -import org.terminal21.client.model.{GlobalEvent, UiEvent} +import org.terminal21.client.model.{GlobalEvent, OnClickController, UiEvent} import org.terminal21.model.CommandEvent trait EventHandler @@ -12,6 +12,9 @@ trait OnClickEventHandler extends EventHandler: object OnClickEventHandler: trait CanHandleOnClickEvent[A <: UiElement]: this: A => + def onClickController[M](handler: ControllerClickEvent[M] => HandledEvent[M]): OnClickController[M] = + OnClickController(this, handler) + def onClick(h: OnClickEventHandler)(using session: ConnectedSession): A = session.addEventHandler(key, h) this diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/collections/EventIterator.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/collections/EventIterator.scala index f8cf1cae..d8df35e0 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/collections/EventIterator.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/collections/EventIterator.scala @@ -2,7 +2,9 @@ package org.terminal21.client.collections import org.terminal21.client.ConnectedSession -class EventIterator[A](it: Iterator[A]) extends Iterator[A]: +import scala.collection.AbstractIterator + +class EventIterator[A](it: Iterator[A]) extends AbstractIterator[A]: override def hasNext: Boolean = it.hasNext override def next(): A = it.next() diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/model/OnClickController.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/model/OnClickController.scala new file mode 100644 index 00000000..eee3f1c3 --- /dev/null +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/model/OnClickController.scala @@ -0,0 +1,10 @@ +package org.terminal21.client.model + +import org.terminal21.client.{ControllerClickEvent, HandledEvent} +import org.terminal21.client.OnClickEventHandler.CanHandleOnClickEvent +import org.terminal21.client.components.UiElement + +case class OnClickController[M]( + element: UiElement & CanHandleOnClickEvent[_], + handler: ControllerClickEvent[M] => HandledEvent[M] +) From 3ecf3f6c637be5f673918b911906827092df6aa6 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 21 Feb 2024 15:55:45 +0000 Subject: [PATCH 107/313] - --- .../scala/org/terminal21/serverapp/bundled/AppManager.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala index 46faf339..c3de77c3 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala @@ -61,12 +61,12 @@ class AppManagerPage(apps: Seq[ServerSideApp], startApp: ServerSideApp => Unit)( ) ) - case class Model(startApp: Option[ServerSideApp]) + case class Model(startApp: Option[ServerSideApp] = None) def controller: Controller[Model] = val clickControllers = appRows.map: appRow => appRow.link.onClickController[Model]: event => - event.handled.withModel(event.model.copy(startApp = Some(appRow.app))) - Controller(Model(None)).onClick(clickControllers) + event.handled.withModel(Model(startApp = Some(appRow.app))) + Controller(Model()).onClick(clickControllers) trait AppManagerBeans: def serverSideSessions: ServerSideSessions From 570694fc8d9573eb88f20081cb6320b2ca76c687 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 21 Feb 2024 16:13:17 +0000 Subject: [PATCH 108/313] - --- build.sbt | 6 +++- .../bundled/AppManagerPageTest.scala | 36 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala diff --git a/build.sbt b/build.sbt index bd987c4a..6a42ac9e 100644 --- a/build.sbt +++ b/build.sbt @@ -110,7 +110,11 @@ lazy val `terminal21-server-app` = project Mockito510 ) ) - .dependsOn(`terminal21-server` % "compile->compile;test->test", `terminal21-ui-std`, `terminal21-server-client-common` % "compile->compile;test->test") + .dependsOn( + `terminal21-server` % "compile->compile;test->test", + `terminal21-ui-std` % "compile->compile;test->test", + `terminal21-server-client-common` % "compile->compile;test->test" + ) lazy val `terminal21-ui-std-exports` = project .settings( diff --git a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala new file mode 100644 index 00000000..d7825899 --- /dev/null +++ b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala @@ -0,0 +1,36 @@ +package org.terminal21.serverapp.bundled + +import org.mockito.Mockito +import org.mockito.Mockito.when +import org.scalatest.funsuite.AnyFunSuiteLike +import org.scalatestplus.mockito.MockitoSugar.mock +import org.terminal21.client.components.chakra.{Link, Text} +import org.terminal21.client.{ConnectedSession, ConnectedSessionMock} +import org.terminal21.serverapp.ServerSideApp +import org.scalatest.matchers.should.Matchers.* + +class AppManagerPageTest extends AnyFunSuiteLike: + def mockApp(name: String, description: String) = + val app = mock[ServerSideApp] + when(app.name).thenReturn(name) + when(app.description).thenReturn(description) + app + + class App(apps: ServerSideApp*): + given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock + val page = new AppManagerPage(apps, _ => ()) + def allComponents = page.components.flatMap(_.flat) + + test("renders app links"): + new App(mockApp("app1", "the-app1-desc")): + allComponents + .collect: + case l: Link if l.text == "app1" => l + .size should be(1) + + test("renders app description"): + new App(mockApp("app1", "the-app1-desc")): + allComponents + .collect: + case t: Text if t.text == "the-app1-desc" => t + .size should be(1) From 33e7b7ace86bc3be51a76e4b2685305fc0b25614 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 21 Feb 2024 17:24:32 +0000 Subject: [PATCH 109/313] - --- .../serverapp/bundled/AppManager.scala | 17 +++++++----- .../bundled/AppManagerPageTest.scala | 27 ++++++++++++++++--- .../terminal21/client/ConnectedSession.scala | 6 ++++- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala index c3de77c3..a2b54bc2 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala @@ -26,10 +26,7 @@ class AppManager(serverSideSessions: ServerSideSessions, fiberExecutor: FiberExe class AppManagerPage(apps: Seq[ServerSideApp], startApp: ServerSideApp => Unit)(using session: ConnectedSession): def run(): Unit = components.render() - controller.eventsIterator - .tapEach: m => - for app <- m.startApp do startApp(app) - .foreach(_ => ()) + eventsIterator.foreach(_ => ()) case class AppRow(app: ServerSideApp, link: Link, text: Text): def row: Seq[UiElement] = Seq(link, text) @@ -62,11 +59,19 @@ class AppManagerPage(apps: Seq[ServerSideApp], startApp: ServerSideApp => Unit)( ) case class Model(startApp: Option[ServerSideApp] = None) - def controller: Controller[Model] = + def controller: Controller[Model] = val clickControllers = appRows.map: appRow => appRow.link.onClickController[Model]: event => event.handled.withModel(Model(startApp = Some(appRow.app))) - Controller(Model()).onClick(clickControllers) + Controller(Model()) + .onClick(clickControllers) + .onEvent: event => + // for every event, initially reset the model + event.handled.withModel(event.model.copy(startApp = None)) + def eventsIterator: Iterator[Model] = + controller.eventsIterator + .tapEach: m => + for app <- m.startApp do startApp(app) trait AppManagerBeans: def serverSideSessions: ServerSideSessions diff --git a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala index d7825899..0ca9e115 100644 --- a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala +++ b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala @@ -4,10 +4,12 @@ import org.mockito.Mockito import org.mockito.Mockito.when import org.scalatest.funsuite.AnyFunSuiteLike import org.scalatestplus.mockito.MockitoSugar.mock +import org.terminal21.client.components.* import org.terminal21.client.components.chakra.{Link, Text} import org.terminal21.client.{ConnectedSession, ConnectedSessionMock} import org.terminal21.serverapp.ServerSideApp import org.scalatest.matchers.should.Matchers.* +import org.terminal21.model.CommandEvent class AppManagerPageTest extends AnyFunSuiteLike: def mockApp(name: String, description: String) = @@ -17,9 +19,10 @@ class AppManagerPageTest extends AnyFunSuiteLike: app class App(apps: ServerSideApp*): - given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock - val page = new AppManagerPage(apps, _ => ()) - def allComponents = page.components.flatMap(_.flat) + given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock + var startedApp: Option[ServerSideApp] = None + val page = new AppManagerPage(apps, app => startedApp = Some(app)) + def allComponents = page.components.flatMap(_.flat) test("renders app links"): new App(mockApp("app1", "the-app1-desc")): @@ -34,3 +37,21 @@ class AppManagerPageTest extends AnyFunSuiteLike: .collect: case t: Text if t.text == "the-app1-desc" => t .size should be(1) + + test("starts app when app link is clicked"): + val app = mockApp("app1", "the-app1-desc") + new App(app): + page.components.render() + val eventsIt = page.eventsIterator + session.fireEvents(CommandEvent.onClick(page.appRows.head.link), CommandEvent.sessionClosed) + eventsIt.toList + startedApp should be(Some(app)) + + test("resets startApp state on other events"): + val app = mockApp("app1", "the-app1-desc") + new App(app): + val other = Link() + (page.components :+ other).render() + val eventsIt = page.eventsIterator + session.fireEvents(CommandEvent.onClick(page.appRows.head.link), CommandEvent.onClick(other), CommandEvent.sessionClosed) + eventsIt.toList.last.startApp should be(None) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index 7a20b6c6..b9b5a3d2 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -102,7 +102,11 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se case (onChange: OnChange, h: OnChangeBooleanEventHandler) => h.onChange(onChange.value.toBoolean) case x => logger.error(s"Unknown event handling combination : $x") case None => // nop - val globalEvent = UiEvent(event, modifiedElements(event.key)) + val globalEvent = + UiEvent( + event, + modifiedElements.getOrElse(event.key, throw new IllegalArgumentException(s"Not found UiElement with key ${event.key}, was this rendered?")) + ) for h <- globalEventHandler do h.onEvent(globalEvent) events.add(globalEvent) catch From 870ffc2e8aad63992f5c9dd9c88be0d28b93667c Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 21 Feb 2024 17:25:38 +0000 Subject: [PATCH 110/313] - --- .../terminal21/serverapp/bundled/AppManagerPageTest.scala | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala index 0ca9e115..9f1b9b73 100644 --- a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala +++ b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala @@ -38,6 +38,13 @@ class AppManagerPageTest extends AnyFunSuiteLike: case t: Text if t.text == "the-app1-desc" => t .size should be(1) + test("renders the discussions link"): + new App(): + allComponents + .collect: + case l: Link if l.href == "https://github.com/kostaskougios/terminal21-restapi/discussions" => l + .size should be(1) + test("starts app when app link is clicked"): val app = mockApp("app1", "the-app1-desc") new App(app): From 43bc8b140dd6786f084e715a302e83ca7b70fb13 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 21 Feb 2024 17:33:23 +0000 Subject: [PATCH 111/313] - --- .../scala/org/terminal21/serverapp/bundled/AppManager.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala index a2b54bc2..02880dd6 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala @@ -59,7 +59,7 @@ class AppManagerPage(apps: Seq[ServerSideApp], startApp: ServerSideApp => Unit)( ) case class Model(startApp: Option[ServerSideApp] = None) - def controller: Controller[Model] = + def controller: Controller[Model] = val clickControllers = appRows.map: appRow => appRow.link.onClickController[Model]: event => event.handled.withModel(Model(startApp = Some(appRow.app))) @@ -68,6 +68,7 @@ class AppManagerPage(apps: Seq[ServerSideApp], startApp: ServerSideApp => Unit)( .onEvent: event => // for every event, initially reset the model event.handled.withModel(event.model.copy(startApp = None)) + def eventsIterator: Iterator[Model] = controller.eventsIterator .tapEach: m => From b9e59ffa8c98b431830a3a0634c240113628c558 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 21 Feb 2024 17:37:48 +0000 Subject: [PATCH 112/313] - --- .../org/terminal21/serverapp/bundled/AppManager.scala | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala index 02880dd6..1d31da9d 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala @@ -60,13 +60,12 @@ class AppManagerPage(apps: Seq[ServerSideApp], startApp: ServerSideApp => Unit)( case class Model(startApp: Option[ServerSideApp] = None) def controller: Controller[Model] = - val clickControllers = appRows.map: appRow => - appRow.link.onClickController[Model]: event => - event.handled.withModel(Model(startApp = Some(appRow.app))) - Controller(Model()) - .onClick(clickControllers) + appRows + .foldLeft(Controller(Model())): (c, appRow) => + c.onClick(appRow.link): event => + event.handled.withModel(Model(startApp = Some(appRow.app))) .onEvent: event => - // for every event, initially reset the model + // for every event, reset the model event.handled.withModel(event.model.copy(startApp = None)) def eventsIterator: Iterator[Model] = From 6b07292a571d58c2915853161a03a65023a5374c Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 21 Feb 2024 17:38:34 +0000 Subject: [PATCH 113/313] - --- .../main/scala/org/terminal21/client/Controller.scala | 7 +------ .../scala/org/terminal21/client/EventHandler.scala | 5 +---- .../terminal21/client/model/OnClickController.scala | 10 ---------- 3 files changed, 2 insertions(+), 20 deletions(-) delete mode 100644 terminal21-ui-std/src/main/scala/org/terminal21/client/model/OnClickController.scala diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 9e3f0de2..fa182a43 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -4,7 +4,7 @@ import org.terminal21.client.OnChangeEventHandler.CanHandleOnChangeEvent import org.terminal21.client.OnClickEventHandler.CanHandleOnClickEvent import org.terminal21.client.collections.EventIterator import org.terminal21.client.components.UiElement -import org.terminal21.client.model.{GlobalEvent, OnClickController, UiEvent} +import org.terminal21.client.model.{GlobalEvent, UiEvent} import org.terminal21.model.{OnChange, OnClick} class Controller[M]( @@ -19,11 +19,6 @@ class Controller[M]( def onEvent(handler: ControllerEvent[M] => HandledEvent[M]) = new Controller(eventIteratorFactory, renderChanges, initialModel, eventHandlers :+ handler, clickHandlers, changeHandlers, changeBooleanHandlers) - def onClick(controller: OnClickController[M]): Controller[M] = onClick(controller.element)(controller.handler) - def onClick(controllers: Seq[OnClickController[M]]): Controller[M] = - controllers.foldLeft(this): (c, h) => - c.onClick(h.element)(h.handler) - def onClick(element: UiElement & CanHandleOnClickEvent[_])(handler: ControllerClickEvent[M] => HandledEvent[M]): Controller[M] = onClicked(element)(handler) def onClicked(elements: UiElement & CanHandleOnClickEvent[_]*)(handler: ControllerClickEvent[M] => HandledEvent[M]): Controller[M] = new Controller( diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala index 693846e2..b2644e89 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala @@ -1,7 +1,7 @@ package org.terminal21.client import org.terminal21.client.components.UiElement -import org.terminal21.client.model.{GlobalEvent, OnClickController, UiEvent} +import org.terminal21.client.model.{GlobalEvent, UiEvent} import org.terminal21.model.CommandEvent trait EventHandler @@ -12,9 +12,6 @@ trait OnClickEventHandler extends EventHandler: object OnClickEventHandler: trait CanHandleOnClickEvent[A <: UiElement]: this: A => - def onClickController[M](handler: ControllerClickEvent[M] => HandledEvent[M]): OnClickController[M] = - OnClickController(this, handler) - def onClick(h: OnClickEventHandler)(using session: ConnectedSession): A = session.addEventHandler(key, h) this diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/model/OnClickController.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/model/OnClickController.scala deleted file mode 100644 index eee3f1c3..00000000 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/model/OnClickController.scala +++ /dev/null @@ -1,10 +0,0 @@ -package org.terminal21.client.model - -import org.terminal21.client.{ControllerClickEvent, HandledEvent} -import org.terminal21.client.OnClickEventHandler.CanHandleOnClickEvent -import org.terminal21.client.components.UiElement - -case class OnClickController[M]( - element: UiElement & CanHandleOnClickEvent[_], - handler: ControllerClickEvent[M] => HandledEvent[M] -) From da098ecf35c055fc6eb78eaeb9856541f2aa7c38 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 21 Feb 2024 18:13:22 +0000 Subject: [PATCH 114/313] - --- .../serverapp/bundled/SettingsApp.scala | 20 ++++++++-------- .../serverapp/bundled/SettingsPageTest.scala | 23 +++++++++++++++++++ .../org/terminal21/collections/SEList.scala | 8 ++++++- .../terminal21/client/ConnectedSession.scala | 7 ++++++ 4 files changed, 48 insertions(+), 10 deletions(-) create mode 100644 terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/SettingsPageTest.scala diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala index 3ef4d5a2..75be0f0b 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala @@ -1,6 +1,6 @@ package org.terminal21.serverapp.bundled -import org.terminal21.client.ConnectedSession +import org.terminal21.client.{ConnectedSession, Controller} import org.terminal21.client.components.* import org.terminal21.client.components.chakra.{ExternalLinkIcon, Link} import org.terminal21.client.components.std.{Paragraph, Span} @@ -17,14 +17,16 @@ class SettingsApp extends ServerSideApp: override def createSession(serverSideSessions: ServerSideSessions, dependencies: Dependencies): Unit = serverSideSessions .withNewSession("frontend-settings", "Settings") - .andOptions(SessionOptions(closeTabWhenTerminated = true)) .connect: session => given ConnectedSession = session - new SettingsAppInstance().run() + new SettingsPage().run() -class SettingsAppInstance(using session: ConnectedSession): - def run() = - Seq( - ThemeToggle() - ).render() - session.waitTillUserClosesSession() +class SettingsPage(using session: ConnectedSession): + val themeToggle = ThemeToggle() + def run() = + components.render() + controller.eventsIterator.lastOption + + def components = Seq(themeToggle) + + def controller = Controller(()) diff --git a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/SettingsPageTest.scala b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/SettingsPageTest.scala new file mode 100644 index 00000000..834e1ce7 --- /dev/null +++ b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/SettingsPageTest.scala @@ -0,0 +1,23 @@ +package org.terminal21.serverapp.bundled + +import org.scalatest.funsuite.AnyFunSuiteLike +import org.terminal21.client.{*, given} +import org.scalatest.matchers.should.Matchers.* +import org.terminal21.model.CommandEvent + +class SettingsPageTest extends AnyFunSuiteLike: + class App: + given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock + val page = new SettingsPage + + test("Should render the ThemeToggle component"): + new App: + page.components should contain(page.themeToggle) + + test("run() should render all components"): + new App: + fiberExecutor.submit: + page.run() + session.waitUntilAtLeast1EventIteratorWasCreated() + session.fireEvents(CommandEvent.sessionClosed) + session.currentlyRendered should be(page.components) diff --git a/terminal21-server-client-common/src/main/scala/org/terminal21/collections/SEList.scala b/terminal21-server-client-common/src/main/scala/org/terminal21/collections/SEList.scala index fc78afe7..a3830e1e 100644 --- a/terminal21-server-client-common/src/main/scala/org/terminal21/collections/SEList.scala +++ b/terminal21-server-client-common/src/main/scala/org/terminal21/collections/SEList.scala @@ -5,10 +5,16 @@ import java.util.concurrent.CountDownLatch class SEList[A]: @volatile private var currentNode: NormalNode[A] = NormalNode(None, EndNode) + private val atLeastOneIterator = new CountDownLatch(1) + /** @return * A new iterator that only reads elements that are added before the iterator is created. */ - def iterator: SEBlockingIterator[A] = new SEBlockingIterator(currentNode) + def iterator: SEBlockingIterator[A] = + atLeastOneIterator.countDown() + new SEBlockingIterator(currentNode) + + def waitUntilAtLeast1IteratorWasCreated(): Unit = atLeastOneIterator.await() /** Add a poison pill to terminate all iterators. */ diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index b9b5a3d2..11f261cc 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -78,6 +78,11 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se def eventIterator: Iterator[GlobalEvent] = events.iterator + /** Waits until at least 1 event iterator was created for the current page. Useful for testing purposes if i.e. one thread runs the main loop and gets an + * eventIterator at some point and an other thread needs to fire events. + */ + def waitUntilAtLeast1EventIteratorWasCreated(): Unit = events.waitUntilAtLeast1IteratorWasCreated() + /** removes the global event handler (if any). No more events will be received by that handler. */ def removeGlobalEventHandler(): Unit = globalEventHandler = None @@ -157,3 +162,5 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se modifiedElements += e.key -> e def currentState[A <: UiElement](e: A): A = modifiedElements.getOrElse(e.key, throw new IllegalStateException(s"Key ${e.key} doesn't exist or was removed")).asInstanceOf[A] + + def currentlyRendered: Seq[UiElement] = modifiedElements.values.toSeq From 08503087516f49f73837227d3691382cdffa84ca Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 21 Feb 2024 19:56:58 +0000 Subject: [PATCH 115/313] - --- .../serverapp/bundled/ServerStatusApp.scala | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 09ad0a22..940b5a6f 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -17,24 +17,26 @@ class ServerStatusApp extends ServerSideApp: override def createSession(serverSideSessions: ServerSideSessions, dependencies: Dependencies): Unit = serverSideSessions .withNewSession("server-status", "Server Status") - .andOptions(SessionOptions(closeTabWhenTerminated = true)) .connect: session => given ConnectedSession = session - new ServerStatusAppInternal(serverSideSessions, dependencies.sessionsService, dependencies.fiberExecutor).run() + new ServerStatusPage(serverSideSessions, dependencies.sessionsService, dependencies.fiberExecutor).run() -class ServerStatusAppInternal(serverSideSessions: ServerSideSessions, sessionsService: ServerSessionsService, executor: FiberExecutor)(using - session: ConnectedSession -): +class ServerStatusPage( + serverSideSessions: ServerSideSessions, + sessionsService: ServerSessionsService, + executor: FiberExecutor +)(using session: ConnectedSession): def run(): Unit = - executor.submit: - while !session.isClosed do - updateStatus() - Thread.sleep(1000) - session.waitTillUserClosesSession() + while !session.isClosed do + updateStatus() + Thread.sleep(1000) private def toMb(v: Long) = s"${v / (1024 * 1024)} MB" private val xs = Some("2xs") private def updateStatus(): Unit = + components.render() + + def components: Seq[UiElement] = val runtime = Runtime.getRuntime val jvmTable = QuickTable(caption = Some("JVM")) @@ -60,7 +62,7 @@ class ServerStatusAppInternal(serverSideSessions: ServerSideSessions, sessionsSe ) .withHeaders("Id", "Name", "Is Open", "Actions") - Seq(jvmTable, sessionsTable).render() + Seq(jvmTable, sessionsTable) private def actionsFor(session: Session)(using ConnectedSession): UiElement = if session.isOpen then @@ -78,11 +80,11 @@ class ServerStatusAppInternal(serverSideSessions: ServerSideSessions, sessionsSe serverSideSessions .withNewSession(session.id + "-server-state", s"Server State:${session.id}") .connect: sSession => - new ViewServerState(sSession).runFor(sessionsService.sessionStateOf(session)) + new ViewServerStatePage(sSession).runFor(sessionsService.sessionStateOf(session)) ) else NotAllowedIcon() -class ViewServerState(session: ConnectedSession): +class ViewServerStatePage(session: ConnectedSession): given ConnectedSession = session def runFor(state: SessionState): Unit = From 93e0eca617d257d3b596c38bcb93b26d1c2af83b Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 21 Feb 2024 20:36:01 +0000 Subject: [PATCH 116/313] - --- .../serverapp/bundled/ServerStatusApp.scala | 12 ++++-------- .../org/terminal21/client/ConnectedSession.scala | 10 +++++----- .../terminal21/client/components/extensions.scala | 4 ++-- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 940b5a6f..d2187379 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -19,12 +19,11 @@ class ServerStatusApp extends ServerSideApp: .withNewSession("server-status", "Server Status") .connect: session => given ConnectedSession = session - new ServerStatusPage(serverSideSessions, dependencies.sessionsService, dependencies.fiberExecutor).run() + new ServerStatusPage(serverSideSessions, dependencies.sessionsService).run() class ServerStatusPage( serverSideSessions: ServerSideSessions, - sessionsService: ServerSessionsService, - executor: FiberExecutor + sessionsService: ServerSessionsService )(using session: ConnectedSession): def run(): Unit = while !session.isClosed do @@ -34,11 +33,9 @@ class ServerStatusPage( private def toMb(v: Long) = s"${v / (1024 * 1024)} MB" private val xs = Some("2xs") private def updateStatus(): Unit = - components.render() - - def components: Seq[UiElement] = - val runtime = Runtime.getRuntime + components(Runtime.getRuntime, sessionsService.allSessions).render() + def components(runtime: Runtime, sessions: Seq[Session]): Seq[UiElement] = val jvmTable = QuickTable(caption = Some("JVM")) .withHeaders("Property", "Value", "Actions") .withRows( @@ -54,7 +51,6 @@ class ServerStatusPage( Seq("Available processors", runtime.availableProcessors(), "") ) ) - val sessions = sessionsService.allSessions val sessionsTable = QuickTable( caption = Some("All sessions"), rows = sessions.map: session => diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index 11f261cc..15bc06e5 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -26,9 +26,9 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se /** Clears all UI elements and event handlers. Renders a blank UI */ def clear(): Unit = + removeGlobalEventHandler() handlers.clear() modifiedElements.clear() - removeGlobalEventHandler() events.poisonPill() events = SEList() @@ -76,6 +76,10 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se def withGlobalEventHandler(h: GlobalEventHandler): Unit = globalEventHandler = Some(h) + /** removes the global event handler (if any). No more events will be received by that handler. + */ + def removeGlobalEventHandler(): Unit = globalEventHandler = None + def eventIterator: Iterator[GlobalEvent] = events.iterator /** Waits until at least 1 event iterator was created for the current page. Useful for testing purposes if i.e. one thread runs the main loop and gets an @@ -83,10 +87,6 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se */ def waitUntilAtLeast1EventIteratorWasCreated(): Unit = events.waitUntilAtLeast1IteratorWasCreated() - /** removes the global event handler (if any). No more events will be received by that handler. - */ - def removeGlobalEventHandler(): Unit = globalEventHandler = None - def fireEvents(events: CommandEvent*): Unit = for e <- events do fireEvent(e) def fireEvent(event: CommandEvent): Unit = diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/extensions.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/extensions.scala index c0159356..003d52ea 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/extensions.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/extensions.scala @@ -4,7 +4,7 @@ import org.terminal21.client.ConnectedSession extension (s: Seq[UiElement]) def render()(using session: ConnectedSession): Unit = - session.render(s: _*) + session.render(s*) def renderChanges()(using session: ConnectedSession): Unit = - session.renderChanges(s: _*) + session.renderChanges(s*) From 19eafc1cf5684675cf5332557cdfd7f03dc66e41 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 22 Feb 2024 11:27:36 +0000 Subject: [PATCH 117/313] - --- .../org/terminal21/client/components/mathjax/MathJaxLib.scala | 2 +- .../main/scala/org/terminal21/client/components/NivoLib.scala | 2 +- .../scala/org/terminal21/serverapp/ServerSideSessions.scala | 3 ++- .../main/scala/org/terminal21/client/ConnectedSession.scala | 3 ++- .../src/main/scala/org/terminal21/client/Sessions.scala | 3 ++- .../client/{components => json}/UiElementEncoding.scala | 3 ++- .../scala/org/terminal21/client/ConnectedSessionMock.scala | 2 +- 7 files changed, 11 insertions(+), 7 deletions(-) rename terminal21-ui-std/src/main/scala/org/terminal21/client/{components => json}/UiElementEncoding.scala (93%) diff --git a/terminal21-mathjax/src/main/scala/org/terminal21/client/components/mathjax/MathJaxLib.scala b/terminal21-mathjax/src/main/scala/org/terminal21/client/components/mathjax/MathJaxLib.scala index 5946f253..e9747117 100644 --- a/terminal21-mathjax/src/main/scala/org/terminal21/client/components/mathjax/MathJaxLib.scala +++ b/terminal21-mathjax/src/main/scala/org/terminal21/client/components/mathjax/MathJaxLib.scala @@ -6,6 +6,6 @@ import io.circe.* import org.terminal21.client.components.{ComponentLib, UiElement} object MathJaxLib extends ComponentLib: - import org.terminal21.client.components.StdElementEncoding.given + import org.terminal21.client.json.StdElementEncoding.given override def toJson(using Encoder[UiElement]): PartialFunction[UiElement, Json] = case n: MathJaxElement => n.asJson.mapObject(o => o.add("type", "MathJax".asJson)) diff --git a/terminal21-nivo/src/main/scala/org/terminal21/client/components/NivoLib.scala b/terminal21-nivo/src/main/scala/org/terminal21/client/components/NivoLib.scala index 5141c4d5..97d595a5 100644 --- a/terminal21-nivo/src/main/scala/org/terminal21/client/components/NivoLib.scala +++ b/terminal21-nivo/src/main/scala/org/terminal21/client/components/NivoLib.scala @@ -6,6 +6,6 @@ import io.circe.syntax.* import org.terminal21.client.components.nivo.NEJson object NivoLib extends ComponentLib: - import org.terminal21.client.components.StdElementEncoding.given + import org.terminal21.client.json.StdElementEncoding.given override def toJson(using Encoder[UiElement]): PartialFunction[UiElement, Json] = case n: NEJson => n.asJson.mapObject(o => o.add("type", "Nivo".asJson)) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideSessions.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideSessions.scala index 413932f9..5f0ed820 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideSessions.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideSessions.scala @@ -2,7 +2,8 @@ package org.terminal21.serverapp import functions.fibers.FiberExecutor import org.terminal21.client.ConnectedSession -import org.terminal21.client.components.{ComponentLib, StdElementEncoding, UiElementEncoding} +import org.terminal21.client.components.ComponentLib +import org.terminal21.client.json.{StdElementEncoding, UiElementEncoding} import org.terminal21.config.Config import org.terminal21.model.SessionOptions import org.terminal21.server.service.ServerSessionsService diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index 15bc06e5..7109bf05 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -4,8 +4,9 @@ import io.circe.* import io.circe.generic.auto.* import org.slf4j.LoggerFactory import org.terminal21.client.components.UiElement.HasChildren -import org.terminal21.client.components.{UiComponent, UiElement, UiElementEncoding} +import org.terminal21.client.components.{UiComponent, UiElement} import org.terminal21.client.internal.EventHandlers +import org.terminal21.client.json.UiElementEncoding import org.terminal21.client.model.{GlobalEvent, SessionClosedEvent, UiEvent} import org.terminal21.collections.SEList import org.terminal21.model.* diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Sessions.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Sessions.scala index 94309c73..23d62381 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Sessions.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Sessions.scala @@ -4,7 +4,8 @@ import functions.fibers.FiberExecutor import functions.helidon.transport.HelidonTransport import io.helidon.webclient.api.WebClient import io.helidon.webclient.websocket.WsClient -import org.terminal21.client.components.{ComponentLib, StdElementEncoding, UiElementEncoding} +import org.terminal21.client.components.ComponentLib +import org.terminal21.client.json.{StdElementEncoding, UiElementEncoding} import org.terminal21.config.Config import org.terminal21.model.SessionOptions import org.terminal21.ui.std.SessionsServiceCallerFactory diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElementEncoding.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/json/UiElementEncoding.scala similarity index 93% rename from terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElementEncoding.scala rename to terminal21-ui-std/src/main/scala/org/terminal21/client/json/UiElementEncoding.scala index 97f6f37b..eb9d0090 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElementEncoding.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/json/UiElementEncoding.scala @@ -1,4 +1,4 @@ -package org.terminal21.client.components +package org.terminal21.client.json import io.circe.* import io.circe.generic.auto.* @@ -6,6 +6,7 @@ import io.circe.syntax.* import org.terminal21.client.components.chakra.{Box, CEJson, ChakraElement} import org.terminal21.client.components.std.{StdEJson, StdElement, StdHttp} import org.terminal21.client.components.ui.FrontEndElement +import org.terminal21.client.components.{ComponentLib, UiComponent, UiElement} class UiElementEncoding(libs: Seq[ComponentLib]): given uiElementEncoder: Encoder[UiElement] = diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionMock.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionMock.scala index 6323b0c7..49bd33d6 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionMock.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionMock.scala @@ -1,7 +1,7 @@ package org.terminal21.client import org.mockito.Mockito.mock -import org.terminal21.client.components.{StdElementEncoding, UiElementEncoding} +import org.terminal21.client.json.{StdElementEncoding, UiElementEncoding} import org.terminal21.model.CommonModelBuilders.session import org.terminal21.ui.std.SessionsService From 295c2583c87105954faca0ea73540402748cbc15 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 22 Feb 2024 12:23:59 +0000 Subject: [PATCH 118/313] - --- .../terminal21/client/ConnectedSession.scala | 14 ++++ .../org/terminal21/client/EventHandler.scala | 15 ++-- .../client/collections/TypedMap.scala | 11 +++ .../client/components/UiElement.scala | 7 ++ .../components/chakra/ChakraElement.scala | 53 +++++++----- .../client/json/UiElementEncoding.scala | 3 + .../scala/generator/GenerateIconsCode.scala | 82 ------------------- .../client/collections/TypedMapTest.scala | 19 +++++ .../client/json/UiElementEncodingTest.scala | 12 +++ 9 files changed, 106 insertions(+), 110 deletions(-) create mode 100644 terminal21-ui-std/src/main/scala/org/terminal21/client/collections/TypedMap.scala delete mode 100644 terminal21-ui-std/src/test/scala/generator/GenerateIconsCode.scala create mode 100644 terminal21-ui-std/src/test/scala/org/terminal21/client/collections/TypedMapTest.scala create mode 100644 terminal21-ui-std/src/test/scala/org/terminal21/client/json/UiElementEncodingTest.scala diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index 7109bf05..752f59ca 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -91,6 +91,12 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se def fireEvents(events: CommandEvent*): Unit = for e <- events do fireEvent(e) def fireEvent(event: CommandEvent): Unit = + val renderedHandlers = modifiedElements.values + .collect: + case h: OnClickEventHandler.CanHandleOnClickEvent[_] => (h.key, h.dataStore.getOrElse(OnClickEventHandler.Key, Nil)) + .toMap + .withDefault(_ => Nil) + try event match case SessionClosed(_) => @@ -99,6 +105,14 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se exitLatch.countDown() onCloseHandler() case _ => + for handler <- renderedHandlers(event.key) do + (event, handler) match + case (_: OnClick, h: OnClickEventHandler) => h.onClick() + case (onChange: OnChange, h: OnChangeEventHandler) => h.onChange(onChange.value) + case (onChange: OnChange, h: OnChangeBooleanEventHandler) => h.onChange(onChange.value.toBoolean) + case x => logger.error(s"Unknown event handling combination : $x") + + // TODO:DROP handlers.getEventHandler(event.key) match case Some(handlers) => for handler <- handlers do diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala index b2644e89..e05b92ce 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala @@ -1,8 +1,9 @@ package org.terminal21.client +import org.terminal21.client.collections.TypedMapKey import org.terminal21.client.components.UiElement -import org.terminal21.client.model.{GlobalEvent, UiEvent} -import org.terminal21.model.CommandEvent +import org.terminal21.client.components.UiElement.HasDataStore +import org.terminal21.client.model.UiEvent trait EventHandler @@ -10,11 +11,13 @@ trait OnClickEventHandler extends EventHandler: def onClick(): Unit object OnClickEventHandler: - trait CanHandleOnClickEvent[A <: UiElement]: + object Key extends TypedMapKey[Seq[OnClickEventHandler]] + + trait CanHandleOnClickEvent[A <: UiElement] extends HasDataStore[A]: this: A => - def onClick(h: OnClickEventHandler)(using session: ConnectedSession): A = - session.addEventHandler(key, h) - this + def onClick(h: OnClickEventHandler): A = + val handlers = dataStore.getOrElse(Key, Nil) + store(Key, handlers :+ h) trait OnChangeEventHandler extends EventHandler: def onChange(newValue: String): Unit diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/collections/TypedMap.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/collections/TypedMap.scala new file mode 100644 index 00000000..86ac5675 --- /dev/null +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/collections/TypedMap.scala @@ -0,0 +1,11 @@ +package org.terminal21.client.collections + +class TypedMap(val m: Map[TypedMapKey[_], Any]): + def +[A](kv: (TypedMapKey[A], A)): TypedMap = new TypedMap(m + kv) + def apply[A](k: TypedMapKey[A]): A = m(k).asInstanceOf[A] + def getOrElse[A](k: TypedMapKey[A], default: => A) = m.getOrElse(k, default).asInstanceOf[A] + +object TypedMap: + def empty = new TypedMap(Map.empty) + +trait TypedMapKey[A] diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala index 86212b34..cabf1337 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala @@ -1,5 +1,6 @@ package org.terminal21.client.components +import org.terminal21.client.collections.{TypedMap, TypedMapKey} import org.terminal21.client.{ConnectedSession, EventHandler} trait UiElement extends AnyElement: @@ -38,3 +39,9 @@ object UiElement: def style: Map[String, Any] def withStyle(v: Map[String, Any]): A def withStyle(vs: (String, Any)*): A = withStyle(vs.toMap) + + trait HasDataStore[A <: UiElement]: + this: A => + def dataStore: TypedMap + def withDataStore(ds: TypedMap): A + def store[V](key: TypedMapKey[V], value: V): A = withDataStore(dataStore + (key -> value)) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala index f8e46605..0b424d97 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala @@ -1,5 +1,6 @@ package org.terminal21.client.components.chakra +import org.terminal21.client.collections.TypedMap import org.terminal21.client.components.UiElement.{Current, HasChildren, HasEventHandler, HasStyle} import org.terminal21.client.components.{Keys, UiElement} import org.terminal21.client.{ConnectedSession, OnChangeBooleanEventHandler, OnChangeEventHandler, OnClickEventHandler} @@ -27,7 +28,8 @@ case class Button( isDisabled: Option[Boolean] = None, isLoading: Option[Boolean] = None, isAttached: Option[Boolean] = None, - spacing: Option[String] = None + spacing: Option[String] = None, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement[Button] with OnClickEventHandler.CanHandleOnClickEvent[Button]: override def withStyle(v: Map[String, Any]): Button = copy(style = v) @@ -45,6 +47,7 @@ case class Button( def withIsLoading(v: Option[Boolean]) = copy(isLoading = v) def withIsAttached(v: Option[Boolean]) = copy(isAttached = v) def withSpacing(v: Option[String]) = copy(spacing = v) + override def withDataStore(ds: TypedMap): Button = copy(dataStore = ds) /** https://chakra-ui.com/docs/components/button */ @@ -1601,14 +1604,16 @@ case class MenuItem( key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, text: String = "", - children: Seq[UiElement] = Nil + children: Seq[UiElement] = Nil, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement[MenuItem] with HasChildren[MenuItem] with OnClickEventHandler.CanHandleOnClickEvent[MenuItem]: - override def withChildren(cn: UiElement*) = copy(children = cn) - override def withStyle(v: Map[String, Any]) = copy(style = v) - def withKey(v: String) = copy(key = v) - def withText(v: String) = copy(text = v) + override def withChildren(cn: UiElement*) = copy(children = cn) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withText(v: String) = copy(text = v) + override def withDataStore(ds: TypedMap): MenuItem = copy(dataStore = ds) case class MenuDivider(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty) extends ChakraElement[MenuDivider]: override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1933,15 +1938,17 @@ case class BreadcrumbLink( text: String = "breadcrumblink.text", href: Option[String] = None, style: Map[String, Any] = Map.empty, - children: Seq[UiElement] = Nil + children: Seq[UiElement] = Nil, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement[BreadcrumbLink] with HasChildren[BreadcrumbLink] with OnClickEventHandler.CanHandleOnClickEvent[BreadcrumbLink]: - def withKey(v: String) = copy(key = v) - override def withChildren(cn: UiElement*) = copy(children = cn) - override def withStyle(v: Map[String, Any]) = copy(style = v) - def withHref(v: Option[String]) = copy(href = v) - def withText(v: String) = copy(text = v) + def withKey(v: String) = copy(key = v) + override def withChildren(cn: UiElement*) = copy(children = cn) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withHref(v: Option[String]) = copy(href = v) + def withText(v: String) = copy(text = v) + override def withDataStore(ds: TypedMap): BreadcrumbLink = copy(dataStore = ds) case class Link( key: String = Keys.nextKey, @@ -1950,16 +1957,18 @@ case class Link( isExternal: Option[Boolean] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - children: Seq[UiElement] = Nil + children: Seq[UiElement] = Nil, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement[Link] with HasChildren[Link] with OnClickEventHandler.CanHandleOnClickEvent[Link]: - def withKey(v: String) = copy(key = v) - override def withChildren(cn: UiElement*) = copy(children = cn) - override def withStyle(v: Map[String, Any]) = copy(style = v) - def withIsExternal(v: Option[Boolean]) = copy(isExternal = v) - def withIsExternal(v: Boolean) = copy(isExternal = Some(v)) - def withHref(v: String) = copy(href = v) - def withText(v: String) = copy(text = v) - def withColor(v: String) = copy(color = Some(v)) - def withColor(v: Option[String]) = copy(color = v) + def withKey(v: String) = copy(key = v) + override def withChildren(cn: UiElement*) = copy(children = cn) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withIsExternal(v: Option[Boolean]) = copy(isExternal = v) + def withIsExternal(v: Boolean) = copy(isExternal = Some(v)) + def withHref(v: String) = copy(href = v) + def withText(v: String) = copy(text = v) + def withColor(v: String) = copy(color = Some(v)) + def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap): Link = copy(dataStore = ds) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/json/UiElementEncoding.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/json/UiElementEncoding.scala index eb9d0090..81a23177 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/json/UiElementEncoding.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/json/UiElementEncoding.scala @@ -3,6 +3,7 @@ package org.terminal21.client.json import io.circe.* import io.circe.generic.auto.* import io.circe.syntax.* +import org.terminal21.client.collections.TypedMap import org.terminal21.client.components.chakra.{Box, CEJson, ChakraElement} import org.terminal21.client.components.std.{StdEJson, StdElement, StdHttp} import org.terminal21.client.components.ui.FrontEndElement @@ -31,6 +32,8 @@ object StdElementEncoding extends ComponentLib: ) Json.obj(vs: _*) + given Encoder[TypedMap] = _ => Json.Null + override def toJson(using Encoder[UiElement]): PartialFunction[UiElement, Json] = case std: StdEJson => std.asJson.mapObject(o => o.add("type", "Std".asJson)) case c: CEJson => c.asJson.mapObject(o => o.add("type", "Chakra".asJson)) diff --git a/terminal21-ui-std/src/test/scala/generator/GenerateIconsCode.scala b/terminal21-ui-std/src/test/scala/generator/GenerateIconsCode.scala deleted file mode 100644 index 5d6da43c..00000000 --- a/terminal21-ui-std/src/test/scala/generator/GenerateIconsCode.scala +++ /dev/null @@ -1,82 +0,0 @@ -package generator - -@main def generateIconsCode() = - val icons = Seq( - "AddIcon", - "ArrowBackIcon", - "ArrowDownIcon", - "ArrowForwardIcon", - "ArrowLeftIcon", - "ArrowRightIcon", - "ArrowUpIcon", - "ArrowUpDownIcon", - "AtSignIcon", - "AttachmentIcon", - "BellIcon", - "CalendarIcon", - "ChatIcon", - "CheckIcon", - "CheckCircleIcon", - "ChevronDownIcon", - "ChevronLeftIcon", - "ChevronRightIcon", - "ChevronUpIcon", - "CloseIcon", - "CopyIcon", - "DeleteIcon", - "DownloadIcon", - "DragHandleIcon", - "EditIcon", - "EmailIcon", - "ExternalLinkIcon", - "HamburgerIcon", - "InfoIcon", - "InfoOutlineIcon", - "LinkIcon", - "LockIcon", - "MinusIcon", - "MoonIcon", - "NotAllowedIcon", - "PhoneIcon", - "PlusSquareIcon", - "QuestionIcon", - "QuestionOutlineIcon", - "RepeatIcon", - "RepeatClockIcon", - "SearchIcon", - "Search2Icon", - "SettingsIcon", - "SmallAddIcon", - "SmallCloseIcon", - "SpinnerIcon", - "StarIcon", - "SunIcon", - "TimeIcon", - "TriangleDownIcon", - "TriangleUpIcon", - "UnlockIcon", - "UpDownIcon", - "ViewIcon", - "ViewOffIcon", - "WarningIcon", - "WarningTwoIcon" - ) - - println("// ------------------------ SCALA CODE -------------------------------------") - for clz <- icons do println(s""" - |/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon - | */ - |case class $clz( - | key: String = Keys.nextKey, - | @volatile var w: Option[String] = None, - | @volatile var h: Option[String] = None, - | @volatile var boxSize: Option[String] = None, - | @volatile var color: Option[String] = None - |) extends ChakraElement - | - |""".stripMargin) - println("// ------------------------ SCALA USING CODE -------------------------------------") - println(icons.map(i => s"$i()").mkString(",")) - println("// ------------------------ TSX CODE -------------------------------------") - println(s"import { ${icons.mkString(",")} } from '@chakra-ui/icons';") - for clz <- icons do println(s"$clz: (b:any) => (<$clz {...b}/>),") diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/collections/TypedMapTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/collections/TypedMapTest.scala new file mode 100644 index 00000000..5f073689 --- /dev/null +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/collections/TypedMapTest.scala @@ -0,0 +1,19 @@ +package org.terminal21.client.collections + +import org.scalatest.funsuite.AnyFunSuiteLike +import org.scalatest.matchers.should.Matchers.* + +class TypedMapTest extends AnyFunSuiteLike: + object IntKey extends TypedMapKey[Int] + object StringKey extends TypedMapKey[String] + + test("add and get"): + val m = TypedMap.empty + (IntKey -> 5) + (StringKey -> "x") + m(IntKey) should be(5) + m(StringKey) should be("x") + + test("getOrElse when key not available"): + TypedMap.empty.getOrElse(IntKey, 2) should be(2) + + test("getOrElse when key available"): + (TypedMap.empty + (IntKey -> 5)).getOrElse(IntKey, 2) should be(5) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/json/UiElementEncodingTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/json/UiElementEncodingTest.scala new file mode 100644 index 00000000..a79cdccc --- /dev/null +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/json/UiElementEncodingTest.scala @@ -0,0 +1,12 @@ +package org.terminal21.client.json + +import org.scalatest.funsuite.AnyFunSuiteLike +import org.terminal21.client.components.chakra.Button +import org.scalatest.matchers.should.Matchers.* + +class UiElementEncodingTest extends AnyFunSuiteLike: + val encoding = new UiElementEncoding(Seq(StdElementEncoding)) + test("dataStore"): + val b = Button() + val j = encoding.uiElementEncoder(b).deepDropNullValues + j.hcursor.downField("Button").downField("dataStore").failed should be(true) From 928292caeab4b81557e6bdbae288c5a6179c059e Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 22 Feb 2024 12:52:33 +0000 Subject: [PATCH 119/313] - --- .../src/main/scala/tests/StdComponents.scala | 3 +- .../main/scala/tests/chakra/Editables.scala | 12 ++-- .../src/main/scala/tests/chakra/Forms.scala | 56 +++++++++---------- .../terminal21/client/ConnectedSession.scala | 7 ++- .../org/terminal21/client/EventHandler.scala | 21 +++---- .../components/chakra/ChakraElement.scala | 28 +++++++--- .../client/components/std/StdElement.scala | 5 +- .../client/components/std/StdHttp.scala | 5 +- 8 files changed, 81 insertions(+), 56 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/StdComponents.scala b/end-to-end-tests/src/main/scala/tests/StdComponents.scala index 0d8dc93c..17f576a3 100644 --- a/end-to-end-tests/src/main/scala/tests/StdComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/StdComponents.scala @@ -10,10 +10,9 @@ import org.terminal21.client.components.std.* .connect: session => given ConnectedSession = session - val input = Input(defaultValue = "Please enter your name") val output = Paragraph(text = "This will reflect what you type in the input") val cookieValue = Paragraph(text = "This will display the value of the cookie") - input.onChange: newValue => + val input = Input(defaultValue = "Please enter your name").onChange: newValue => output.withText(newValue).renderChanges() Seq( diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala b/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala index 505f5edf..9cb06032 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala @@ -9,20 +9,20 @@ object Editables: def components(using session: ConnectedSession): Seq[UiElement] = val status = Box(text = "This will reflect any changes in the form.") - val editable1 = Editable(defaultValue = "Please type here").withChildren( + val editable1I = Editable(defaultValue = "Please type here").withChildren( EditablePreview(), EditableInput() ) - editable1.onChange: newValue => - status.withText(s"editable1 newValue = $newValue, verify editable1.value = ${editable1.current.value}").renderChanges() + val editable1 = editable1I.onChange: newValue => + status.withText(s"editable1 newValue = $newValue, verify editable1.value = ${editable1I.current.value}").renderChanges() - val editable2 = Editable(defaultValue = "For longer maybe-editable texts\nUse an EditableTextarea\nIt uses a textarea control.").withChildren( + val editable2I = Editable(defaultValue = "For longer maybe-editable texts\nUse an EditableTextarea\nIt uses a textarea control.").withChildren( EditablePreview(), EditableTextarea() ) - editable2.onChange: newValue => - status.withText(s"editable2 newValue = $newValue, verify editable2.value = ${editable2.current.value}").renderChanges() + val editable2 = editable2I.onChange: newValue => + status.withText(s"editable2 newValue = $newValue, verify editable2.value = ${editable2I.current.value}").renderChanges() Seq( commonBox(text = "Editables"), diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala b/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala index 296bbe18..aa987954 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala @@ -13,24 +13,24 @@ object Forms: val emailRightAddOn = InputRightAddon().withChildren(okIcon) - val email = Input(`type` = "email", defaultValue = "my@email.com") - email.onChange: newValue => + val emailI = Input(`type` = "email", defaultValue = "my@email.com") + val email = emailI.onChange: newValue => Seq( - status.withText(s"email input new value = $newValue, verify email.value = ${email.current.value}"), + status.withText(s"email input new value = $newValue, verify email.value = ${emailI.current.value}"), if newValue.contains("@") then emailRightAddOn.withChildren(okIcon) else emailRightAddOn.withChildren(notOkIcon) ).renderChanges() - val description = Textarea(placeholder = "Please enter a few things about you", defaultValue = "desc") - description.onChange: newValue => - status.withText(s"description input new value = $newValue, verify description.value = ${description.current.value}").renderChanges() + val descriptionI = Textarea(placeholder = "Please enter a few things about you", defaultValue = "desc") + val description = descriptionI.onChange: newValue => + status.withText(s"description input new value = $newValue, verify description.value = ${descriptionI.current.value}").renderChanges() - val select1 = Select(placeholder = "Please choose").withChildren( + val select1I = Select(placeholder = "Please choose").withChildren( Option_(text = "Male", value = "male"), Option_(text = "Female", value = "female") ) - select1.onChange: newValue => - status.withText(s"select1 input new value = $newValue, verify select1.value = ${select1.current.value}").renderChanges() + val select1 = select1I.onChange: newValue => + status.withText(s"select1 input new value = $newValue, verify select1.value = ${select1I.current.value}").renderChanges() val select2 = Select(defaultValue = "1", bg = Some("tomato"), color = Some("black"), borderColor = Some("yellow")).withChildren( Option_(text = "First", value = "1"), @@ -38,36 +38,36 @@ object Forms: ) val password = Input(`type` = "password", defaultValue = "mysecret") - val dob = Input(`type` = "datetime-local") - dob.onChange: newValue => - status.withText(s"dob = $newValue , verify dob.value = ${dob.current.value}").renderChanges() + val dobI = Input(`type` = "datetime-local") + val dob = dobI.onChange: newValue => + status.withText(s"dob = $newValue , verify dob.value = ${dobI.current.value}").renderChanges() - val color = Input(`type` = "color") + val colorI = Input(`type` = "color") - color.onChange: newValue => - status.withText(s"color = $newValue , verify color.value = ${color.current.value}").renderChanges() + val color = colorI.onChange: newValue => + status.withText(s"color = $newValue , verify color.value = ${colorI.current.value}").renderChanges() - val checkbox2 = Checkbox(text = "Check 2", defaultChecked = true) - checkbox2.onChange: newValue => - status.withText(s"checkbox2 checked is $newValue , verify checkbox2.checked = ${checkbox2.current.checked}").renderChanges() + val checkbox2I = Checkbox(text = "Check 2", defaultChecked = true) + val checkbox2 = checkbox2I.onChange: newValue => + status.withText(s"checkbox2 checked is $newValue , verify checkbox2.checked = ${checkbox2I.current.checked}").renderChanges() - val checkbox1 = Checkbox(text = "Check 1") - checkbox1.onChange: newValue => + val checkbox1I = Checkbox(text = "Check 1") + val checkbox1 = checkbox1I.onChange: newValue => Seq( - status.withText(s"checkbox1 checked is $newValue , verify checkbox1.checked = ${checkbox1.current.checked}"), + status.withText(s"checkbox1 checked is $newValue , verify checkbox1.checked = ${checkbox1I.current.checked}"), checkbox2.withIsDisabled(newValue) ).renderChanges() - val switch1 = Switch(text = "Switch 1") - val switch2 = Switch(text = "Switch 2", defaultChecked = true) + val switch1I = Switch(text = "Switch 1") + val switch2 = Switch(text = "Switch 2", defaultChecked = true) - switch1.onChange: newValue => + val switch1 = switch1I.onChange: newValue => Seq( - status.withText(s"switch1 checked is $newValue , verify switch1.checked = ${switch1.current.checked}"), + status.withText(s"switch1 checked is $newValue , verify switch1.checked = ${switch1I.current.checked}"), switch2.withIsDisabled(newValue) ).renderChanges() - val radioGroup = RadioGroup(defaultValue = "2").withChildren( + val radioGroupI = RadioGroup(defaultValue = "2").withChildren( HStack().withChildren( Radio(value = "1", text = "first"), Radio(value = "2", text = "second"), @@ -75,8 +75,8 @@ object Forms: ) ) - radioGroup.onChange: newValue => - status.withText(s"radioGroup newValue=$newValue , verify radioGroup.value=${radioGroup.current.value}").renderChanges() + val radioGroup = radioGroupI.onChange: newValue => + status.withText(s"radioGroup newValue=$newValue , verify radioGroup.value=${radioGroupI.current.value}").renderChanges() Seq( commonBox(text = "Forms"), diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index 752f59ca..539b6b12 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -92,8 +92,13 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se def fireEvent(event: CommandEvent): Unit = val renderedHandlers = modifiedElements.values + .flatMap(_.flat) .collect: - case h: OnClickEventHandler.CanHandleOnClickEvent[_] => (h.key, h.dataStore.getOrElse(OnClickEventHandler.Key, Nil)) + case h: OnClickEventHandler.CanHandleOnClickEvent[_] => (h.key, h.dataStore.getOrElse(OnClickEventHandler.Key, Nil)) + case h: OnChangeEventHandler.CanHandleOnChangeEvent[_] => + (h.key, h.defaultEventHandler(this) +: h.dataStore.getOrElse(OnChangeEventHandler.Key, Nil)) + case h: OnChangeBooleanEventHandler.CanHandleOnChangeEvent[_] => + (h.key, h.defaultEventHandler(this) +: h.dataStore.getOrElse(OnChangeBooleanEventHandler.Key, Nil)) .toMap .withDefault(_ => Nil) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala index e05b92ce..2e8a032a 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala @@ -2,7 +2,7 @@ package org.terminal21.client import org.terminal21.client.collections.TypedMapKey import org.terminal21.client.components.UiElement -import org.terminal21.client.components.UiElement.HasDataStore +import org.terminal21.client.components.UiElement.{HasDataStore, HasEventHandler} import org.terminal21.client.model.UiEvent trait EventHandler @@ -11,8 +11,7 @@ trait OnClickEventHandler extends EventHandler: def onClick(): Unit object OnClickEventHandler: - object Key extends TypedMapKey[Seq[OnClickEventHandler]] - + object Key extends TypedMapKey[Seq[OnClickEventHandler]] trait CanHandleOnClickEvent[A <: UiElement] extends HasDataStore[A]: this: A => def onClick(h: OnClickEventHandler): A = @@ -23,21 +22,23 @@ trait OnChangeEventHandler extends EventHandler: def onChange(newValue: String): Unit object OnChangeEventHandler: - trait CanHandleOnChangeEvent[A <: UiElement]: + object Key extends TypedMapKey[Seq[OnChangeEventHandler]] + trait CanHandleOnChangeEvent[A <: UiElement] extends HasDataStore[A] with HasEventHandler: this: A => - def onChange(h: OnChangeEventHandler)(using session: ConnectedSession): A = - session.addEventHandler(key, h) - this + def onChange(h: OnChangeEventHandler): A = + val handlers = dataStore.getOrElse(Key, Nil) + store(Key, handlers :+ h) trait OnChangeBooleanEventHandler extends EventHandler: def onChange(newValue: Boolean): Unit object OnChangeBooleanEventHandler: - trait CanHandleOnChangeEvent[A <: UiElement]: + object Key extends TypedMapKey[Seq[OnChangeBooleanEventHandler]] + trait CanHandleOnChangeEvent[A <: UiElement] extends HasDataStore[A] with HasEventHandler: this: A => def onChange(h: OnChangeBooleanEventHandler)(using session: ConnectedSession): A = - session.addEventHandler(key, h) - this + val handlers = dataStore.getOrElse(Key, Nil) + store(Key, handlers :+ h) trait GlobalEventHandler extends EventHandler: def onEvent(event: UiEvent): Unit diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala index 0b424d97..7c1c4a7c 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala @@ -154,7 +154,8 @@ case class Editable( defaultValue: String = "", valueReceived: Option[String] = None, // use value instead style: Map[String, Any] = Map.empty, - children: Seq[UiElement] = Nil + children: Seq[UiElement] = Nil, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement[Editable] with HasEventHandler with HasChildren[Editable] @@ -166,6 +167,7 @@ case class Editable( def withKey(v: String) = copy(key = v) def withDefaultValue(v: String) = copy(defaultValue = v) def value = valueReceived.getOrElse(defaultValue) + override def withDataStore(ds: TypedMap): Editable = copy(dataStore = ds) case class EditablePreview(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty) extends ChakraElement[EditablePreview]: override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -231,7 +233,8 @@ case class Input( variant: Option[String] = None, defaultValue: String = "", valueReceived: Option[String] = None, // use value instead - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement[Input] with HasEventHandler with OnChangeEventHandler.CanHandleOnChangeEvent[Input]: @@ -244,6 +247,7 @@ case class Input( def withVariant(v: Option[String]): Input = copy(variant = v) def withDefaultValue(v: String): Input = copy(defaultValue = v) def value: String = valueReceived.getOrElse(defaultValue) + override def withDataStore(ds: TypedMap): Input = copy(dataStore = ds) case class InputGroup( key: String = Keys.nextKey, @@ -289,7 +293,8 @@ case class Checkbox( defaultChecked: Boolean = false, isDisabled: Boolean = false, style: Map[String, Any] = Map.empty, - checkedV: Option[Boolean] = None + checkedV: Option[Boolean] = None, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement[Checkbox] with HasEventHandler with OnChangeBooleanEventHandler.CanHandleOnChangeEvent[Checkbox]: @@ -300,6 +305,7 @@ case class Checkbox( def withText(v: String) = copy(text = v) def withDefaultChecked(v: Boolean) = copy(defaultChecked = v) def withIsDisabled(v: Boolean) = copy(isDisabled = v) + override def withDataStore(ds: TypedMap): Checkbox = copy(dataStore = ds) /** https://chakra-ui.com/docs/components/radio */ @@ -321,7 +327,8 @@ case class RadioGroup( defaultValue: String = "", valueReceived: Option[String] = None, // use value instead style: Map[String, Any] = Map.empty, - children: Seq[UiElement] = Nil + children: Seq[UiElement] = Nil, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement[RadioGroup] with HasEventHandler with HasChildren[RadioGroup] @@ -332,6 +339,7 @@ case class RadioGroup( def value: String = valueReceived.getOrElse(defaultValue) def withKey(v: String) = copy(key = v) def withDefaultValue(v: String) = copy(defaultValue = v) + override def withDataStore(ds: TypedMap): RadioGroup = copy(dataStore = ds) case class Center( key: String = Keys.nextKey, @@ -1391,7 +1399,8 @@ case class Textarea( variant: Option[String] = None, defaultValue: String = "", valueReceived: Option[String] = None, // use value instead - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement[Textarea] with HasEventHandler with OnChangeEventHandler.CanHandleOnChangeEvent[Textarea]: @@ -1404,6 +1413,7 @@ case class Textarea( def withVariant(v: Option[String]) = copy(variant = v) def withDefaultValue(v: String) = copy(defaultValue = v) def value = valueReceived.getOrElse(defaultValue) + override def withDataStore(ds: TypedMap): Textarea = copy(dataStore = ds) /** https://chakra-ui.com/docs/components/switch */ @@ -1413,7 +1423,8 @@ case class Switch( defaultChecked: Boolean = false, isDisabled: Boolean = false, style: Map[String, Any] = Map.empty, - checkedV: Option[Boolean] = None // use checked + checkedV: Option[Boolean] = None, // use checked + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement[Switch] with HasEventHandler with OnChangeBooleanEventHandler.CanHandleOnChangeEvent[Switch]: @@ -1424,6 +1435,7 @@ case class Switch( def withText(v: String) = copy(text = v) def withDefaultChecked(v: Boolean) = copy(defaultChecked = v) def withIsDisabled(v: Boolean) = copy(isDisabled = v) + override def withDataStore(ds: TypedMap): Switch = copy(dataStore = ds) /** https://chakra-ui.com/docs/components/select */ @@ -1436,7 +1448,8 @@ case class Select( color: Option[String] = None, borderColor: Option[String] = None, style: Map[String, Any] = Map.empty, - children: Seq[UiElement] = Nil + children: Seq[UiElement] = Nil, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement[Select] with HasEventHandler with HasChildren[Select] @@ -1451,6 +1464,7 @@ case class Select( def withColor(v: Option[String]) = copy(color = v) def withBorderColor(v: Option[String]) = copy(borderColor = v) def value = valueReceived.getOrElse(defaultValue) + override def withDataStore(ds: TypedMap): Select = copy(dataStore = ds) case class Option_( key: String = Keys.nextKey, diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala index 1a71d84a..8e2dad5f 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala @@ -1,6 +1,7 @@ package org.terminal21.client.components.std import org.terminal21.client.OnChangeEventHandler.CanHandleOnChangeEvent +import org.terminal21.client.collections.TypedMap import org.terminal21.client.components.UiElement.{Current, HasChildren, HasEventHandler, HasStyle} import org.terminal21.client.components.{Keys, UiElement} import org.terminal21.client.{ConnectedSession, OnChangeEventHandler} @@ -69,7 +70,8 @@ case class Input( `type`: String = "text", defaultValue: String = "", style: Map[String, Any] = Map.empty, - valueReceived: Option[String] = None // use value instead + valueReceived: Option[String] = None, // use value instead + dataStore: TypedMap = TypedMap.empty ) extends StdElement[Input] with HasEventHandler with CanHandleOnChangeEvent[Input]: @@ -79,3 +81,4 @@ case class Input( def withType(v: String) = copy(`type` = v) def withDefaultValue(v: String) = copy(defaultValue = v) def value = valueReceived.getOrElse(defaultValue) + override def withDataStore(ds: TypedMap): Input = copy(dataStore = ds) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala index 71762997..a3eb94fa 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala @@ -1,6 +1,7 @@ package org.terminal21.client.components.std import org.terminal21.client.OnChangeEventHandler.CanHandleOnChangeEvent +import org.terminal21.client.collections.TypedMap import org.terminal21.client.{ConnectedSession, EventHandler, OnChangeEventHandler} import org.terminal21.client.components.UiElement.HasEventHandler import org.terminal21.client.components.{Keys, TransientRequest, UiElement} @@ -39,8 +40,10 @@ case class CookieReader( key: String = Keys.nextKey, name: String = "cookie.name", value: Option[String] = None, // will be set when/if cookie value is read - requestId: String = TransientRequest.newRequestId() + requestId: String = TransientRequest.newRequestId(), + dataStore: TypedMap = TypedMap.empty ) extends StdHttp with HasEventHandler with CanHandleOnChangeEvent[CookieReader]: override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = newValue => session.modified(copy(value = Some(newValue))) + override def withDataStore(ds: TypedMap): CookieReader = copy(dataStore = ds) From 76bb8bb6fd408b0923a2c440127e3c6b4eba5ac6 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 22 Feb 2024 12:55:11 +0000 Subject: [PATCH 120/313] - --- .../terminal21/client/ConnectedSession.scala | 19 +------------ .../client/internal/EventHandlers.scala | 28 ------------------- 2 files changed, 1 insertion(+), 46 deletions(-) delete mode 100644 terminal21-ui-std/src/main/scala/org/terminal21/client/internal/EventHandlers.scala diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index 539b6b12..4e1f6f2f 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -5,7 +5,6 @@ import io.circe.generic.auto.* import org.slf4j.LoggerFactory import org.terminal21.client.components.UiElement.HasChildren import org.terminal21.client.components.{UiComponent, UiElement} -import org.terminal21.client.internal.EventHandlers import org.terminal21.client.json.UiElementEncoding import org.terminal21.client.model.{GlobalEvent, SessionClosedEvent, UiEvent} import org.terminal21.collections.SEList @@ -19,7 +18,6 @@ import scala.collection.concurrent.TrieMap class ConnectedSession(val session: Session, encoding: UiElementEncoding, val serverUrl: String, sessionsService: SessionsService, onCloseHandler: () => Unit): private val logger = LoggerFactory.getLogger(getClass) - private val handlers = new EventHandlers(this) @volatile private var events = SEList[GlobalEvent]() def uiUrl: String = serverUrl + "/ui" @@ -28,13 +26,10 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se */ def clear(): Unit = removeGlobalEventHandler() - handlers.clear() modifiedElements.clear() events.poisonPill() events = SEList() - def addEventHandler(key: String, handler: EventHandler): Unit = handlers.addEventHandler(key, handler) - private val exitLatch = new CountDownLatch(1) /** Waits till user closes the session by clicking the session close [X] button. @@ -116,23 +111,12 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se case (onChange: OnChange, h: OnChangeEventHandler) => h.onChange(onChange.value) case (onChange: OnChange, h: OnChangeBooleanEventHandler) => h.onChange(onChange.value.toBoolean) case x => logger.error(s"Unknown event handling combination : $x") - - // TODO:DROP - handlers.getEventHandler(event.key) match - case Some(handlers) => - for handler <- handlers do - (event, handler) match - case (_: OnClick, h: OnClickEventHandler) => h.onClick() - case (onChange: OnChange, h: OnChangeEventHandler) => h.onChange(onChange.value) - case (onChange: OnChange, h: OnChangeBooleanEventHandler) => h.onChange(onChange.value.toBoolean) - case x => logger.error(s"Unknown event handling combination : $x") - case None => // nop val globalEvent = UiEvent( event, modifiedElements.getOrElse(event.key, throw new IllegalArgumentException(s"Not found UiElement with key ${event.key}, was this rendered?")) ) - for h <- globalEventHandler do h.onEvent(globalEvent) + for h <- globalEventHandler do h.onEvent(globalEvent) events.add(globalEvent) catch case t: Throwable => @@ -141,7 +125,6 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se def render(es: UiElement*): Unit = for e <- es.flatMap(_.flat) do modified(e) - handlers.registerEventHandlers(es) val j = toJson(es) sessionsService.setSessionJsonState(session, j) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/internal/EventHandlers.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/internal/EventHandlers.scala deleted file mode 100644 index 3146b74f..00000000 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/internal/EventHandlers.scala +++ /dev/null @@ -1,28 +0,0 @@ -package org.terminal21.client.internal - -import org.terminal21.client.components.UiElement -import org.terminal21.client.components.UiElement.HasEventHandler -import org.terminal21.client.{ConnectedSession, EventHandler} - -class EventHandlers(session: ConnectedSession): - private val eventHandlers = collection.concurrent.TrieMap.empty[String, List[EventHandler]] - - def registerEventHandlers(es: Seq[UiElement]): Unit = synchronized: - val all = es.flatMap(_.flat) - val withEvents = all.collect: - case h: HasEventHandler => h - - for e <- withEvents do addEventHandlerAtTheTop(e.key, e.defaultEventHandler(session)) - - def addEventHandler(key: String, handler: EventHandler): Unit = - val handlers = eventHandlers.getOrElse(key, Nil) - eventHandlers += key -> (handlers :+ handler) - - def getEventHandler(key: String): Option[List[EventHandler]] = eventHandlers.get(key) - - private def addEventHandlerAtTheTop(key: String, handler: EventHandler): Unit = - val handlers = eventHandlers.getOrElse(key, Nil) - eventHandlers += key -> (handler :: handlers) - - def clear(): Unit = synchronized: - eventHandlers.clear() From 741d155c6eddbefc65339c76ef1a592d7a49d145 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 22 Feb 2024 13:05:46 +0000 Subject: [PATCH 121/313] - --- .../src/main/scala/tests/RunAll.scala | 15 +++++++++++++++ .../src/main/scala/tests/chakra/Forms.scala | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 end-to-end-tests/src/main/scala/tests/RunAll.scala diff --git a/end-to-end-tests/src/main/scala/tests/RunAll.scala b/end-to-end-tests/src/main/scala/tests/RunAll.scala new file mode 100644 index 00000000..a734bbf9 --- /dev/null +++ b/end-to-end-tests/src/main/scala/tests/RunAll.scala @@ -0,0 +1,15 @@ +package tests + +import org.terminal21.client.given + +@main def runAll(): Unit = + Seq( + fiberExecutor.submit: + chakraComponents() + , + fiberExecutor.submit: + stdComponents() + , + fiberExecutor.submit: + loginFormApp() + ).foreach(_.get()) diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala b/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala index aa987954..d968a394 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala @@ -13,7 +13,7 @@ object Forms: val emailRightAddOn = InputRightAddon().withChildren(okIcon) - val emailI = Input(`type` = "email", defaultValue = "my@email.com") + val emailI = Input(`type` = "email", defaultValue = "the-test-email@email.com") val email = emailI.onChange: newValue => Seq( status.withText(s"email input new value = $newValue, verify email.value = ${emailI.current.value}"), @@ -81,7 +81,7 @@ object Forms: Seq( commonBox(text = "Forms"), FormControl().withChildren( - FormLabel(text = "Email address"), + FormLabel(text = "Test-Email-Address"), InputGroup().withChildren( InputLeftAddon().withChildren(EmailIcon()), email, From 6636a0f47ca46a2969058cbdf80f262cb3bc0182 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 22 Feb 2024 13:16:22 +0000 Subject: [PATCH 122/313] - --- .../client/ConnectedSessionTest.scala | 37 +++++++++++++++++-- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala index 67c0fe2f..16a171bd 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala @@ -5,12 +5,14 @@ import org.mockito.Mockito.verify import org.scalatest.funsuite.AnyFunSuiteLike import org.scalatest.matchers.should.Matchers.* import org.terminal21.client.ConnectedSessionMock.encoder -import org.terminal21.client.components.chakra.Editable +import org.terminal21.client.components.chakra.{Button, Checkbox, Editable, Input} import org.terminal21.client.components.std.{Paragraph, Span} import org.terminal21.client.model.UiEvent import org.terminal21.model.{CommandEvent, OnChange} import org.terminal21.ui.std.ServerJson +import java.util.concurrent.atomic.AtomicBoolean + class ConnectedSessionTest extends AnyFunSuiteLike: test("global event iterator"): @@ -42,11 +44,38 @@ class ConnectedSessionTest extends AnyFunSuiteLike: connectedSession.fireEvent(event) received should be(Some(event)) + test("click events are processed by onClick handlers"): + given connectedSession: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock + var clicked = false + val button = Button().onClick: () => + clicked = true + connectedSession.render(button) + connectedSession.fireEvent(CommandEvent.onClick(button)) + clicked should be(true) + + test("change events are processed by onChange handlers"): + given connectedSession: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock + var value = "" + val input = Input().onChange: newValue => + value = newValue + connectedSession.render(input) + connectedSession.fireEvent(CommandEvent.onChange(input, "new-value")) + value should be("new-value") + + test("change boolean events are processed by onChange handlers"): + given connectedSession: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock + var value = false + val checkbox = Checkbox().onChange: newValue => + value = newValue + connectedSession.render(checkbox) + connectedSession.fireEvent(CommandEvent.onChange(checkbox, true)) + value should be(true) + test("default event handlers are invoked before user handlers"): given connectedSession: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock - val editable = Editable() - editable.onChange: newValue => - editable.current.value should be(newValue) + val editableI = Editable() + val editable = editableI.onChange: newValue => + editableI.current.value should be(newValue) connectedSession.render(editable) connectedSession.fireEvent(OnChange(editable.key, "new value")) From 3498ba76064cfaedeafe0e5ef4776c1952adb3e5 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 22 Feb 2024 13:25:57 +0000 Subject: [PATCH 123/313] - --- .../src/main/scala/org/terminal21/client/EventHandler.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala index 2e8a032a..a3e0d6df 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala @@ -36,7 +36,7 @@ object OnChangeBooleanEventHandler: object Key extends TypedMapKey[Seq[OnChangeBooleanEventHandler]] trait CanHandleOnChangeEvent[A <: UiElement] extends HasDataStore[A] with HasEventHandler: this: A => - def onChange(h: OnChangeBooleanEventHandler)(using session: ConnectedSession): A = + def onChange(h: OnChangeBooleanEventHandler): A = val handlers = dataStore.getOrElse(Key, Nil) store(Key, handlers :+ h) From fe0a5d48e96cf082d821a024f6a34401ccfed011 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 22 Feb 2024 13:57:42 +0000 Subject: [PATCH 124/313] - --- .../terminal21/serverapp/bundled/ServerStatusApp.scala | 8 +++----- .../org/terminal21/serverapp/bundled/SettingsApp.scala | 5 +---- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index d2187379..879f8f8f 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -1,10 +1,9 @@ package org.terminal21.serverapp.bundled -import functions.fibers.FiberExecutor import org.terminal21.client.ConnectedSession import org.terminal21.client.components.* import org.terminal21.client.components.chakra.* -import org.terminal21.model.{Session, SessionOptions} +import org.terminal21.model.Session import org.terminal21.server.Dependencies import org.terminal21.server.model.SessionState import org.terminal21.server.service.ServerSessionsService @@ -76,12 +75,11 @@ class ServerStatusPage( serverSideSessions .withNewSession(session.id + "-server-state", s"Server State:${session.id}") .connect: sSession => - new ViewServerStatePage(sSession).runFor(sessionsService.sessionStateOf(session)) + new ViewServerStatePage(using sSession).runFor(sessionsService.sessionStateOf(session)) ) else NotAllowedIcon() -class ViewServerStatePage(session: ConnectedSession): - given ConnectedSession = session +class ViewServerStatePage(using session: ConnectedSession): def runFor(state: SessionState): Unit = val sj = state.serverJson diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala index 75be0f0b..89f6fd1c 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala @@ -1,11 +1,8 @@ package org.terminal21.serverapp.bundled -import org.terminal21.client.{ConnectedSession, Controller} import org.terminal21.client.components.* -import org.terminal21.client.components.chakra.{ExternalLinkIcon, Link} -import org.terminal21.client.components.std.{Paragraph, Span} import org.terminal21.client.components.ui.ThemeToggle -import org.terminal21.model.SessionOptions +import org.terminal21.client.{ConnectedSession, Controller} import org.terminal21.server.Dependencies import org.terminal21.serverapp.{ServerSideApp, ServerSideSessions} From b102d5d7c6afcc76e058ba1829ad4b0b219f4c12 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 22 Feb 2024 14:00:01 +0000 Subject: [PATCH 125/313] - --- .../org/terminal21/serverapp/bundled/ServerStatusApp.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 879f8f8f..df98a60c 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -17,8 +17,7 @@ class ServerStatusApp extends ServerSideApp: serverSideSessions .withNewSession("server-status", "Server Status") .connect: session => - given ConnectedSession = session - new ServerStatusPage(serverSideSessions, dependencies.sessionsService).run() + new ServerStatusPage(serverSideSessions, dependencies.sessionsService)(using session).run() class ServerStatusPage( serverSideSessions: ServerSideSessions, From a35b26b876b8ce5705ad1c65d111c18c08cf3d2f Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 22 Feb 2024 14:10:59 +0000 Subject: [PATCH 126/313] - --- .../org/terminal21/client/ConnectedSession.scala | 16 ---------------- .../terminal21/client/ConnectedSessionTest.scala | 12 ------------ 2 files changed, 28 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index 4e1f6f2f..6af52cde 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -25,7 +25,6 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se /** Clears all UI elements and event handlers. Renders a blank UI */ def clear(): Unit = - removeGlobalEventHandler() modifiedElements.clear() events.poisonPill() events = SEList() @@ -62,20 +61,6 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se def click(e: UiElement): Unit = fireEvent(OnClick(e.key)) - @volatile private var globalEventHandler: Option[GlobalEventHandler] = None - - /** Registers a global event handler who will handle all received events. - * - * @param h - * GlobalEventHandler - */ - def withGlobalEventHandler(h: GlobalEventHandler): Unit = - globalEventHandler = Some(h) - - /** removes the global event handler (if any). No more events will be received by that handler. - */ - def removeGlobalEventHandler(): Unit = globalEventHandler = None - def eventIterator: Iterator[GlobalEvent] = events.iterator /** Waits until at least 1 event iterator was created for the current page. Useful for testing purposes if i.e. one thread runs the main loop and gets an @@ -116,7 +101,6 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se event, modifiedElements.getOrElse(event.key, throw new IllegalArgumentException(s"Not found UiElement with key ${event.key}, was this rendered?")) ) - for h <- globalEventHandler do h.onEvent(globalEvent) events.add(globalEvent) catch case t: Throwable => diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala index 16a171bd..289c6c50 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala @@ -32,18 +32,6 @@ class ConnectedSessionTest extends AnyFunSuiteLike: ) ) - test("global event handler is called on event"): - given connectedSession: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock - val editable = Editable() - editable.render() - var received = Option.empty[CommandEvent] - connectedSession.withGlobalEventHandler: ge => - received = Some(ge.event) - ge.receivedBy should be(editable.copy(valueReceived = Some("new value"))) - val event = OnChange(editable.key, "new value") - connectedSession.fireEvent(event) - received should be(Some(event)) - test("click events are processed by onClick handlers"): given connectedSession: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock var clicked = false From 3cb5c865567601c87cd58426a597a5f07b7a2d8b Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 22 Feb 2024 14:57:14 +0000 Subject: [PATCH 127/313] - --- .../src/main/scala/org/terminal21/model/CommandEvent.scala | 2 ++ .../scala/org/terminal21/client/model/GlobalEvent.scala | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/terminal21-server-client-common/src/main/scala/org/terminal21/model/CommandEvent.scala b/terminal21-server-client-common/src/main/scala/org/terminal21/model/CommandEvent.scala index cc6d473e..c862961e 100644 --- a/terminal21-server-client-common/src/main/scala/org/terminal21/model/CommandEvent.scala +++ b/terminal21-server-client-common/src/main/scala/org/terminal21/model/CommandEvent.scala @@ -2,6 +2,8 @@ package org.terminal21.model import org.terminal21.client.components.AnyElement +/** These are the events as they arrive from the server + */ sealed trait CommandEvent: def key: String diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/model/GlobalEvent.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/model/GlobalEvent.scala index 12fac343..1bdd0330 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/model/GlobalEvent.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/model/GlobalEvent.scala @@ -3,6 +3,8 @@ package org.terminal21.client.model import org.terminal21.client.components.UiElement import org.terminal21.model.{CommandEvent, OnChange, OnClick} +/** These are the events as handled by the std lib. They enrich the server events with local information. + */ sealed trait GlobalEvent: def isTarget(e: UiElement): Boolean def isSessionClose: Boolean @@ -10,8 +12,8 @@ sealed trait GlobalEvent: object GlobalEvent: def onClick(receivedBy: UiElement): UiEvent = UiEvent(OnClick(receivedBy.key), receivedBy) def onChange(receivedBy: UiElement, value: String): UiEvent = UiEvent(OnChange(receivedBy.key, value), receivedBy) - def onChangeEvent(receivedBy: UiElement, value: Boolean): UiEvent = UiEvent(OnChange(receivedBy.key, value.toString), receivedBy) - def sessionClosedEvent: GlobalEvent = SessionClosedEvent + def onChange(receivedBy: UiElement, value: Boolean): UiEvent = UiEvent(OnChange(receivedBy.key, value.toString), receivedBy) + def sessionClosed: GlobalEvent = SessionClosedEvent case class UiEvent(event: CommandEvent, receivedBy: UiElement) extends GlobalEvent: override def isTarget(e: UiElement): Boolean = e.key == receivedBy.key From 2d04bf4c34fb1e2e9fb03d2fe76270e628e9f482 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 22 Feb 2024 14:58:45 +0000 Subject: [PATCH 128/313] - --- .../scala/org/terminal21/serverapp/bundled/SettingsApp.scala | 2 +- .../client/components/{ui => frontend}/FrontEndElement.scala | 2 +- .../scala/org/terminal21/client/json/UiElementEncoding.scala | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename terminal21-ui-std/src/main/scala/org/terminal21/client/components/{ui => frontend}/FrontEndElement.scala (78%) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala index 89f6fd1c..e0719f62 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala @@ -1,7 +1,7 @@ package org.terminal21.serverapp.bundled import org.terminal21.client.components.* -import org.terminal21.client.components.ui.ThemeToggle +import org.terminal21.client.components.frontend.ThemeToggle import org.terminal21.client.{ConnectedSession, Controller} import org.terminal21.server.Dependencies import org.terminal21.serverapp.{ServerSideApp, ServerSideSessions} diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/ui/FrontEndElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/frontend/FrontEndElement.scala similarity index 78% rename from terminal21-ui-std/src/main/scala/org/terminal21/client/components/ui/FrontEndElement.scala rename to terminal21-ui-std/src/main/scala/org/terminal21/client/components/frontend/FrontEndElement.scala index 7e337316..d043758e 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/ui/FrontEndElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/frontend/FrontEndElement.scala @@ -1,4 +1,4 @@ -package org.terminal21.client.components.ui +package org.terminal21.client.components.frontend import org.terminal21.client.components.{Keys, UiElement} diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/json/UiElementEncoding.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/json/UiElementEncoding.scala index 81a23177..7894d69b 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/json/UiElementEncoding.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/json/UiElementEncoding.scala @@ -5,8 +5,8 @@ import io.circe.generic.auto.* import io.circe.syntax.* import org.terminal21.client.collections.TypedMap import org.terminal21.client.components.chakra.{Box, CEJson, ChakraElement} +import org.terminal21.client.components.frontend.FrontEndElement import org.terminal21.client.components.std.{StdEJson, StdElement, StdHttp} -import org.terminal21.client.components.ui.FrontEndElement import org.terminal21.client.components.{ComponentLib, UiComponent, UiElement} class UiElementEncoding(libs: Seq[ComponentLib]): From b10ce065bb43bdf97437c3d69b8374a6ae975ff6 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 22 Feb 2024 15:02:06 +0000 Subject: [PATCH 129/313] - --- .../main/scala/org/terminal21/client/ConnectedSession.scala | 2 +- .../src/main/scala/org/terminal21/client/Controller.scala | 6 +++--- .../terminal21/client/{ => components}/EventHandler.scala | 3 ++- .../scala/org/terminal21/client/components/UiElement.scala | 2 +- .../terminal21/client/components/chakra/ChakraElement.scala | 4 ++-- .../org/terminal21/client/components/std/StdElement.scala | 6 +++--- .../org/terminal21/client/components/std/StdHttp.scala | 6 +++--- 7 files changed, 15 insertions(+), 14 deletions(-) rename terminal21-ui-std/src/main/scala/org/terminal21/client/{ => components}/EventHandler.scala (90%) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index 6af52cde..37268fc2 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -4,7 +4,7 @@ import io.circe.* import io.circe.generic.auto.* import org.slf4j.LoggerFactory import org.terminal21.client.components.UiElement.HasChildren -import org.terminal21.client.components.{UiComponent, UiElement} +import org.terminal21.client.components.{OnChangeBooleanEventHandler, OnChangeEventHandler, OnClickEventHandler, UiComponent, UiElement} import org.terminal21.client.json.UiElementEncoding import org.terminal21.client.model.{GlobalEvent, SessionClosedEvent, UiEvent} import org.terminal21.collections.SEList diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index fa182a43..ce7a3d13 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -1,9 +1,9 @@ package org.terminal21.client -import org.terminal21.client.OnChangeEventHandler.CanHandleOnChangeEvent -import org.terminal21.client.OnClickEventHandler.CanHandleOnClickEvent +import org.terminal21.client.components.OnChangeEventHandler.CanHandleOnChangeEvent +import org.terminal21.client.components.OnClickEventHandler.CanHandleOnClickEvent import org.terminal21.client.collections.EventIterator -import org.terminal21.client.components.UiElement +import org.terminal21.client.components.{OnChangeBooleanEventHandler, OnChangeEventHandler, UiElement} import org.terminal21.client.model.{GlobalEvent, UiEvent} import org.terminal21.model.{OnChange, OnClick} diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala similarity index 90% rename from terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala rename to terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala index a3e0d6df..d883f251 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala @@ -1,9 +1,10 @@ -package org.terminal21.client +package org.terminal21.client.components import org.terminal21.client.collections.TypedMapKey import org.terminal21.client.components.UiElement import org.terminal21.client.components.UiElement.{HasDataStore, HasEventHandler} import org.terminal21.client.model.UiEvent +import org.terminal21.client.components.{EventHandler, OnChangeBooleanEventHandler, OnChangeEventHandler, OnClickEventHandler} trait EventHandler diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala index cabf1337..4b56db1d 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala @@ -1,7 +1,7 @@ package org.terminal21.client.components import org.terminal21.client.collections.{TypedMap, TypedMapKey} -import org.terminal21.client.{ConnectedSession, EventHandler} +import org.terminal21.client.ConnectedSession trait UiElement extends AnyElement: def key: String diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala index 7c1c4a7c..2b30b513 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala @@ -2,8 +2,8 @@ package org.terminal21.client.components.chakra import org.terminal21.client.collections.TypedMap import org.terminal21.client.components.UiElement.{Current, HasChildren, HasEventHandler, HasStyle} -import org.terminal21.client.components.{Keys, UiElement} -import org.terminal21.client.{ConnectedSession, OnChangeBooleanEventHandler, OnChangeEventHandler, OnClickEventHandler} +import org.terminal21.client.components.{Keys, OnChangeBooleanEventHandler, OnChangeEventHandler, OnClickEventHandler, UiElement} +import org.terminal21.client.ConnectedSession sealed trait CEJson extends UiElement diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala index 8e2dad5f..be50fc62 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala @@ -1,10 +1,10 @@ package org.terminal21.client.components.std -import org.terminal21.client.OnChangeEventHandler.CanHandleOnChangeEvent +import org.terminal21.client.components.OnChangeEventHandler.CanHandleOnChangeEvent import org.terminal21.client.collections.TypedMap import org.terminal21.client.components.UiElement.{Current, HasChildren, HasEventHandler, HasStyle} -import org.terminal21.client.components.{Keys, UiElement} -import org.terminal21.client.{ConnectedSession, OnChangeEventHandler} +import org.terminal21.client.components.{Keys, OnChangeEventHandler, UiElement} +import org.terminal21.client.ConnectedSession sealed trait StdEJson extends UiElement sealed trait StdElement[A <: UiElement] extends StdEJson with HasStyle[A] with Current[A] diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala index a3eb94fa..0997c75f 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala @@ -1,10 +1,10 @@ package org.terminal21.client.components.std -import org.terminal21.client.OnChangeEventHandler.CanHandleOnChangeEvent +import org.terminal21.client.components.OnChangeEventHandler.CanHandleOnChangeEvent import org.terminal21.client.collections.TypedMap -import org.terminal21.client.{ConnectedSession, EventHandler, OnChangeEventHandler} +import org.terminal21.client.ConnectedSession import org.terminal21.client.components.UiElement.HasEventHandler -import org.terminal21.client.components.{Keys, TransientRequest, UiElement} +import org.terminal21.client.components.{EventHandler, Keys, OnChangeEventHandler, TransientRequest, UiElement} import org.terminal21.model.OnChange /** Elements mapping to Http functionality From d39f4e68bf2b14b17369fb9999ab7dd0559ae8ff Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 22 Feb 2024 15:02:29 +0000 Subject: [PATCH 130/313] - --- .../scala/org/terminal21/client/components/EventHandler.scala | 2 -- 1 file changed, 2 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala index d883f251..7c4a992f 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala @@ -1,10 +1,8 @@ package org.terminal21.client.components import org.terminal21.client.collections.TypedMapKey -import org.terminal21.client.components.UiElement import org.terminal21.client.components.UiElement.{HasDataStore, HasEventHandler} import org.terminal21.client.model.UiEvent -import org.terminal21.client.components.{EventHandler, OnChangeBooleanEventHandler, OnChangeEventHandler, OnClickEventHandler} trait EventHandler From e5bd0ce328bf1724e5d9ff6631ccfbdc5f23a214 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 22 Feb 2024 21:01:49 +0000 Subject: [PATCH 131/313] - --- .../main/scala/tests/ChakraComponents.scala | 9 +- .../serverapp/bundled/ServerStatusApp.scala | 8 +- .../org/terminal21/model/CommandEvent.scala | 11 +- .../sparklib/CalculationsExtensions.scala | 52 +++--- .../calculations/SparkCalculation.scala | 108 ++++++------ .../terminal21/client/ConnectedSession.scala | 65 ++----- .../org/terminal21/client/Controller.scala | 162 +++++++++--------- .../client/collections/TypedMap.scala | 1 + .../client/components/EventHandler.scala | 40 ++--- .../client/components/StdUiCalculation.scala | 130 +++++++------- .../client/components/UiElement.scala | 14 +- .../components/chakra/ChakraElement.scala | 135 +++++++-------- .../client/components/extensions.scala | 10 -- .../client/components/std/StdElement.scala | 15 +- .../client/components/std/StdHttp.scala | 5 +- .../terminal21/client/model/GlobalEvent.scala | 24 --- .../terminal21/client/CalculationTest.scala | 74 ++++---- .../client/ConnectedSessionTest.scala | 62 ++----- .../terminal21/client/ControllerTest.scala | 84 +++++---- .../client/collections/TypedMapTest.scala | 6 + 20 files changed, 454 insertions(+), 561 deletions(-) delete mode 100644 terminal21-ui-std/src/main/scala/org/terminal21/client/components/extensions.scala delete mode 100644 terminal21-ui-std/src/main/scala/org/terminal21/client/model/GlobalEvent.scala diff --git a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala index c5578cd9..cd959357 100644 --- a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala @@ -18,14 +18,13 @@ import java.util.concurrent.atomic.AtomicBoolean .withNewSession("chakra-components", "Chakra Components") .connect: session => keepRunning.set(false) - given ConnectedSession = session - - val latch = new CountDownLatch(1) + given ConnectedSession = session + given controller: Controller[Boolean] = Controller(false) // react tests reset the session to clear state - val krButton = Button(text = "Reset state").onClick: () => + val krButton = Button(text = "Reset state").onClick: event => keepRunning.set(true) - latch.countDown() + event.handled.terminate (Overlay.components ++ Forms.components ++ Editables.components ++ Stacks.components ++ Grids.components ++ Buttons.components( latch diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index df98a60c..5f1cddb4 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -1,6 +1,6 @@ package org.terminal21.serverapp.bundled -import org.terminal21.client.ConnectedSession +import org.terminal21.client.{ConnectedSession, Controller} import org.terminal21.client.components.* import org.terminal21.client.components.chakra.* import org.terminal21.model.Session @@ -31,9 +31,10 @@ class ServerStatusPage( private def toMb(v: Long) = s"${v / (1024 * 1024)} MB" private val xs = Some("2xs") private def updateStatus(): Unit = + given Controller[Unit] = Controller(()) components(Runtime.getRuntime, sessionsService.allSessions).render() - def components(runtime: Runtime, sessions: Seq[Session]): Seq[UiElement] = + def components(runtime: Runtime, sessions: Seq[Session])(using Controller[Unit]): Seq[UiElement] = val jvmTable = QuickTable(caption = Some("JVM")) .withHeaders("Property", "Value", "Actions") .withRows( @@ -43,8 +44,9 @@ class ServerStatusPage( Seq( "Total Memory", toMb(runtime.totalMemory()), - Button(size = xs, text = "Run GC").onClick: () => + Button(size = xs, text = "Run GC").onClick: event => System.gc() + event.handled ), Seq("Available processors", runtime.availableProcessors(), "") ) diff --git a/terminal21-server-client-common/src/main/scala/org/terminal21/model/CommandEvent.scala b/terminal21-server-client-common/src/main/scala/org/terminal21/model/CommandEvent.scala index c862961e..949ec117 100644 --- a/terminal21-server-client-common/src/main/scala/org/terminal21/model/CommandEvent.scala +++ b/terminal21-server-client-common/src/main/scala/org/terminal21/model/CommandEvent.scala @@ -6,6 +6,7 @@ import org.terminal21.client.components.AnyElement */ sealed trait CommandEvent: def key: String + def isSessionClosed: Boolean object CommandEvent: def onClick(receivedBy: AnyElement): OnClick = OnClick(receivedBy.key) @@ -13,7 +14,11 @@ object CommandEvent: def onChange(receivedBy: AnyElement, value: Boolean): OnChange = OnChange(receivedBy.key, value.toString) def sessionClosed: SessionClosed = SessionClosed("-") -case class OnClick(key: String) extends CommandEvent -case class OnChange(key: String, value: String) extends CommandEvent +case class OnClick(key: String) extends CommandEvent: + override def isSessionClosed: Boolean = false -case class SessionClosed(key: String) extends CommandEvent +case class OnChange(key: String, value: String) extends CommandEvent: + override def isSessionClosed: Boolean = false + +case class SessionClosed(key: String) extends CommandEvent: + override def isSessionClosed: Boolean = true diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/CalculationsExtensions.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/CalculationsExtensions.scala index 2ee3e933..df93c8d9 100644 --- a/terminal21-spark/src/main/scala/org/terminal21/sparklib/CalculationsExtensions.scala +++ b/terminal21-spark/src/main/scala/org/terminal21/sparklib/CalculationsExtensions.scala @@ -1,26 +1,26 @@ -package org.terminal21.sparklib - -import functions.fibers.FiberExecutor -import org.apache.spark.sql.SparkSession -import org.terminal21.client.ConnectedSession -import org.terminal21.client.components.UiElement.HasStyle -import org.terminal21.client.components.{Keys, UiElement} -import org.terminal21.sparklib.calculations.{ReadWriter, StdUiSparkCalculation} - -extension [OUT: ReadWriter](ds: OUT) - def visualize(name: String, dataUi: UiElement with HasStyle[_])( - toUi: OUT => UiElement & HasStyle[_] - )(using - session: ConnectedSession, - executor: FiberExecutor, - spark: SparkSession - ) = - val ui = new StdUiSparkCalculation[OUT](Keys.nextKey, name, dataUi): - override protected def whenResultsReady(results: OUT): Unit = - try updateUi(toUi(results)) - catch case t: Throwable => t.printStackTrace() - super.whenResultsReady(results) - override def nonCachedCalculation: OUT = ds - - ui.run() - ui +//package org.terminal21.sparklib +// +//import functions.fibers.FiberExecutor +//import org.apache.spark.sql.SparkSession +//import org.terminal21.client.ConnectedSession +//import org.terminal21.client.components.UiElement.HasStyle +//import org.terminal21.client.components.{Keys, UiElement} +//import org.terminal21.sparklib.calculations.{ReadWriter, StdUiSparkCalculation} +// +//extension [OUT: ReadWriter](ds: OUT) +// def visualize(name: String, dataUi: UiElement with HasStyle[_])( +// toUi: OUT => UiElement & HasStyle[_] +// )(using +// session: ConnectedSession, +// executor: FiberExecutor, +// spark: SparkSession +// ) = +// val ui = new StdUiSparkCalculation[OUT](Keys.nextKey, name, dataUi): +// override protected def whenResultsReady(results: OUT): Unit = +// try updateUi(toUi(results)) +// catch case t: Throwable => t.printStackTrace() +// super.whenResultsReady(results) +// override def nonCachedCalculation: OUT = ds +// +// ui.run() +// ui diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala index fc7ed440..3b2e664b 100644 --- a/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala +++ b/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala @@ -1,54 +1,54 @@ -package org.terminal21.sparklib.calculations - -import functions.fibers.FiberExecutor -import org.apache.commons.io.FileUtils -import org.apache.spark.sql.SparkSession -import org.terminal21.client.ConnectedSession -import org.terminal21.client.components.UiElement.HasStyle -import org.terminal21.client.components.{CachedCalculation, StdUiCalculation, UiComponent, UiElement} -import org.terminal21.sparklib.util.Environment - -import java.io.File - -/** A UI component that takes a spark calculation (i.e. a spark query) that results in a Dataset. It caches the results by storing them as parquet into the tmp - * folder/spark-calculations/$name. Next time the calculation runs it reads the cache if available. A button should allow the user to clear the cache and rerun - * the spark calculations in case the data changed. - * - * Because the cache is stored in the disk, it is available even if the jvm running the code restarts. This allows the user to run and rerun their code without - * having to rerun the spark calculation. - * - * Subclass this to create your own UI for a spark calculation, see StdUiSparkCalculation below. - */ -trait SparkCalculation[OUT: ReadWriter](name: String)(using executor: FiberExecutor, spark: SparkSession) extends CachedCalculation[OUT] with UiComponent: - private val rw = implicitly[ReadWriter[OUT]] - private val rootFolder = s"${Environment.tmpDirectory}/spark-calculations" - private val targetDir = s"$rootFolder/$name" - - def isCached: Boolean = new File(targetDir).exists() - def cachePath: String = targetDir - - private def cache[A](reader: => A, writer: => A): A = - if isCached then reader - else writer - - override def invalidateCache(): Unit = - FileUtils.deleteDirectory(new File(targetDir)) - - private def calculateOnce(f: => OUT): OUT = - cache( - rw.read(spark, targetDir), { - val ds = f - rw.write(targetDir, ds) - ds - } - ) - - override protected def calculation(): OUT = calculateOnce(nonCachedCalculation) - -abstract class StdUiSparkCalculation[OUT: ReadWriter]( - val key: String, - name: String, - dataUi: UiElement with HasStyle[_] -)(using session: ConnectedSession, executor: FiberExecutor, spark: SparkSession) - extends SparkCalculation[OUT](name) - with StdUiCalculation[OUT](name, dataUi) +//package org.terminal21.sparklib.calculations +// +//import functions.fibers.FiberExecutor +//import org.apache.commons.io.FileUtils +//import org.apache.spark.sql.SparkSession +//import org.terminal21.client.ConnectedSession +//import org.terminal21.client.components.UiElement.HasStyle +//import org.terminal21.client.components.{CachedCalculation, StdUiCalculation, UiComponent, UiElement} +//import org.terminal21.sparklib.util.Environment +// +//import java.io.File +// +///** A UI component that takes a spark calculation (i.e. a spark query) that results in a Dataset. It caches the results by storing them as parquet into the tmp +// * folder/spark-calculations/$name. Next time the calculation runs it reads the cache if available. A button should allow the user to clear the cache and rerun +// * the spark calculations in case the data changed. +// * +// * Because the cache is stored in the disk, it is available even if the jvm running the code restarts. This allows the user to run and rerun their code without +// * having to rerun the spark calculation. +// * +// * Subclass this to create your own UI for a spark calculation, see StdUiSparkCalculation below. +// */ +//trait SparkCalculation[OUT: ReadWriter](name: String)(using executor: FiberExecutor, spark: SparkSession) extends CachedCalculation[OUT] with UiComponent: +// private val rw = implicitly[ReadWriter[OUT]] +// private val rootFolder = s"${Environment.tmpDirectory}/spark-calculations" +// private val targetDir = s"$rootFolder/$name" +// +// def isCached: Boolean = new File(targetDir).exists() +// def cachePath: String = targetDir +// +// private def cache[A](reader: => A, writer: => A): A = +// if isCached then reader +// else writer +// +// override def invalidateCache(): Unit = +// FileUtils.deleteDirectory(new File(targetDir)) +// +// private def calculateOnce(f: => OUT): OUT = +// cache( +// rw.read(spark, targetDir), { +// val ds = f +// rw.write(targetDir, ds) +// ds +// } +// ) +// +// override protected def calculation(): OUT = calculateOnce(nonCachedCalculation) +// +//abstract class StdUiSparkCalculation[OUT: ReadWriter]( +// val key: String, +// name: String, +// dataUi: UiElement with HasStyle[_] +//)(using session: ConnectedSession, executor: FiberExecutor, spark: SparkSession) +// extends SparkCalculation[OUT](name) +// with StdUiCalculation[OUT](name, dataUi) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index 37268fc2..9a5c5922 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -4,9 +4,8 @@ import io.circe.* import io.circe.generic.auto.* import org.slf4j.LoggerFactory import org.terminal21.client.components.UiElement.HasChildren -import org.terminal21.client.components.{OnChangeBooleanEventHandler, OnChangeEventHandler, OnClickEventHandler, UiComponent, UiElement} +import org.terminal21.client.components.{UiComponent, UiElement} import org.terminal21.client.json.UiElementEncoding -import org.terminal21.client.model.{GlobalEvent, SessionClosedEvent, UiEvent} import org.terminal21.collections.SEList import org.terminal21.model.* import org.terminal21.ui.std.{ServerJson, SessionsService} @@ -18,14 +17,13 @@ import scala.collection.concurrent.TrieMap class ConnectedSession(val session: Session, encoding: UiElementEncoding, val serverUrl: String, sessionsService: SessionsService, onCloseHandler: () => Unit): private val logger = LoggerFactory.getLogger(getClass) - @volatile private var events = SEList[GlobalEvent]() + @volatile private var events = SEList[CommandEvent]() def uiUrl: String = serverUrl + "/ui" /** Clears all UI elements and event handlers. Renders a blank UI */ def clear(): Unit = - modifiedElements.clear() events.poisonPill() events = SEList() @@ -61,7 +59,7 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se def click(e: UiElement): Unit = fireEvent(OnClick(e.key)) - def eventIterator: Iterator[GlobalEvent] = events.iterator + def eventIterator: Iterator[CommandEvent] = events.iterator /** Waits until at least 1 event iterator was created for the current page. Useful for testing purposes if i.e. one thread runs the main loop and gets an * eventIterator at some point and an other thread needs to fire events. @@ -71,50 +69,20 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se def fireEvents(events: CommandEvent*): Unit = for e <- events do fireEvent(e) def fireEvent(event: CommandEvent): Unit = - val renderedHandlers = modifiedElements.values - .flatMap(_.flat) - .collect: - case h: OnClickEventHandler.CanHandleOnClickEvent[_] => (h.key, h.dataStore.getOrElse(OnClickEventHandler.Key, Nil)) - case h: OnChangeEventHandler.CanHandleOnChangeEvent[_] => - (h.key, h.defaultEventHandler(this) +: h.dataStore.getOrElse(OnChangeEventHandler.Key, Nil)) - case h: OnChangeBooleanEventHandler.CanHandleOnChangeEvent[_] => - (h.key, h.defaultEventHandler(this) +: h.dataStore.getOrElse(OnChangeBooleanEventHandler.Key, Nil)) - .toMap - .withDefault(_ => Nil) - - try - event match - case SessionClosed(_) => - events.add(SessionClosedEvent) - events.poisonPill() - exitLatch.countDown() - onCloseHandler() - case _ => - for handler <- renderedHandlers(event.key) do - (event, handler) match - case (_: OnClick, h: OnClickEventHandler) => h.onClick() - case (onChange: OnChange, h: OnChangeEventHandler) => h.onChange(onChange.value) - case (onChange: OnChange, h: OnChangeBooleanEventHandler) => h.onChange(onChange.value.toBoolean) - case x => logger.error(s"Unknown event handling combination : $x") - val globalEvent = - UiEvent( - event, - modifiedElements.getOrElse(event.key, throw new IllegalArgumentException(s"Not found UiElement with key ${event.key}, was this rendered?")) - ) - events.add(globalEvent) - catch - case t: Throwable => - logger.error(s"Session ${session.id}: An error occurred while handling $event", t) - throw t - - def render(es: UiElement*): Unit = - for e <- es.flatMap(_.flat) do modified(e) + events.add(event) + event match + case SessionClosed(_) => + events.poisonPill() + exitLatch.countDown() + onCloseHandler() + case _ => + + def render(es: Seq[UiElement]): Unit = val j = toJson(es) sessionsService.setSessionJsonState(session, j) - def renderChanges(es: UiElement*): Unit = + def renderChanges(es: Seq[UiElement]): Unit = if !isClosed && es.nonEmpty then - for e <- es.flatMap(_.flat) do modified(e) val j = toJson(es) sessionsService.changeSessionJsonState(session, j) @@ -144,10 +112,3 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se .toMap ) sj - private val modifiedElements = TrieMap.empty[String, UiElement] - def modified(e: UiElement): Unit = - modifiedElements += e.key -> e - def currentState[A <: UiElement](e: A): A = - modifiedElements.getOrElse(e.key, throw new IllegalStateException(s"Key ${e.key} doesn't exist or was removed")).asInstanceOf[A] - - def currentlyRendered: Seq[UiElement] = modifiedElements.values.toSeq diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index ce7a3d13..ec65d298 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -1,99 +1,90 @@ package org.terminal21.client +import org.terminal21.client.collections.{EventIterator, TypedMapKey} import org.terminal21.client.components.OnChangeEventHandler.CanHandleOnChangeEvent import org.terminal21.client.components.OnClickEventHandler.CanHandleOnClickEvent -import org.terminal21.client.collections.EventIterator -import org.terminal21.client.components.{OnChangeBooleanEventHandler, OnChangeEventHandler, UiElement} -import org.terminal21.client.model.{GlobalEvent, UiEvent} -import org.terminal21.model.{OnChange, OnClick} +import org.terminal21.client.components.{OnChangeBooleanEventHandler, OnChangeEventHandler, OnClickEventHandler, UiElement} +import org.terminal21.model.{CommandEvent, OnChange, OnClick} class Controller[M]( - eventIteratorFactory: => Iterator[GlobalEvent], + eventIteratorFactory: => Iterator[CommandEvent], renderChanges: Seq[UiElement] => Unit, - initialModel: M, - eventHandlers: Seq[ControllerEvent[M] => HandledEvent[M]], - clickHandlers: Map[String, ControllerClickEvent[M] => HandledEvent[M]], - changeHandlers: Map[String, ControllerChangeEvent[M] => HandledEvent[M]], - changeBooleanHandlers: Map[String, ControllerChangeBooleanEvent[M] => HandledEvent[M]] + components: Seq[UiElement], + initialModel: Model[M], + eventHandlers: Seq[ControllerEvent[M] => HandledEvent[M]] ): def onEvent(handler: ControllerEvent[M] => HandledEvent[M]) = - new Controller(eventIteratorFactory, renderChanges, initialModel, eventHandlers :+ handler, clickHandlers, changeHandlers, changeBooleanHandlers) - - def onClick(element: UiElement & CanHandleOnClickEvent[_])(handler: ControllerClickEvent[M] => HandledEvent[M]): Controller[M] = onClicked(element)(handler) - def onClicked(elements: UiElement & CanHandleOnClickEvent[_]*)(handler: ControllerClickEvent[M] => HandledEvent[M]): Controller[M] = - new Controller( - eventIteratorFactory, - renderChanges, - initialModel, - eventHandlers, - clickHandlers ++ elements.map(e => e.key -> handler), - changeHandlers, - changeBooleanHandlers - ) - - def onChange(element: UiElement & OnChangeEventHandler.CanHandleOnChangeEvent[_])(handler: ControllerChangeEvent[M] => HandledEvent[M]): Controller[M] = - onChanged(element)(handler) - def onChanged(elements: UiElement & OnChangeEventHandler.CanHandleOnChangeEvent[_]*)(handler: ControllerChangeEvent[M] => HandledEvent[M]): Controller[M] = - new Controller( - eventIteratorFactory, - renderChanges, - initialModel, - eventHandlers, - clickHandlers, - changeHandlers ++ elements.map(e => e.key -> handler), - changeBooleanHandlers - ) - - def onChange(element: UiElement & OnChangeBooleanEventHandler.CanHandleOnChangeEvent[_])( - handler: ControllerChangeBooleanEvent[M] => HandledEvent[M] - ): Controller[M] = - onChangedBoolean(element)(handler) - - def onChangedBoolean( - elements: UiElement & OnChangeBooleanEventHandler.CanHandleOnChangeEvent[_]* - )(handler: ControllerChangeBooleanEvent[M] => HandledEvent[M]): Controller[M] = new Controller( eventIteratorFactory, renderChanges, + components, initialModel, - eventHandlers, - clickHandlers, - changeHandlers, - changeBooleanHandlers ++ elements.map(e => e.key -> handler) + eventHandlers :+ handler ) def eventsIterator: EventIterator[M] = new EventIterator(handledEventsIterator.takeWhile(!_.shouldTerminate).map(_.model)) + private def clickHandlersMap(h: HandledEvent[M]): Map[String, Seq[OnClickEventHandlerFunction[M]]] = + h.componentsByKey.values + .collect: + case e: OnClickEventHandler.CanHandleOnClickEvent[_] if e.dataStore.contains(initialModel.ClickKey) => (e.key, e.dataStore(initialModel.ClickKey)) + .toMap + private def changeHandlersMap(h: HandledEvent[M]): Map[String, Seq[OnChangeEventHandlerFunction[M]]] = + h.componentsByKey.values + .collect: + case e: OnChangeEventHandler.CanHandleOnChangeEvent[_] if e.dataStore.contains(initialModel.ChangeKey) => (e.key, e.dataStore(initialModel.ChangeKey)) + .toMap + private def changeBooleanHandlersMap(h: HandledEvent[M]): Map[String, Seq[OnChangeBooleanEventHandlerFunction[M]]] = + h.componentsByKey.values + .collect: + case e: OnChangeBooleanEventHandler.CanHandleOnChangeEvent[_] if e.dataStore.contains(initialModel.ChangeBooleanKey) => + (e.key, e.dataStore(initialModel.ChangeBooleanKey)) + .toMap + def handledEventsIterator: EventIterator[HandledEvent[M]] = + val componentsByKey = + components.flatMap(_.flat).map(c => (c.key, c)).toMap.withDefault(key => throw new IllegalArgumentException(s"Component with key=$key is not available")) + new EventIterator( eventIteratorFactory - .takeWhile(!_.isSessionClose) - .scanLeft(HandledEvent(initialModel, Nil, Nil, false)): (oldHandled, event) => - val h = eventHandlers.foldLeft(oldHandled): (h, f) => + .takeWhile(!_.isSessionClosed) + .scanLeft(HandledEvent(initialModel.value, componentsByKey, Nil, Nil, false)): (oldHandled, event) => + val h = eventHandlers.foldLeft(oldHandled.copy(renderChanges = Nil, timedRenderChanges = Nil)): (h, f) => event match - case UiEvent(OnClick(_), receivedBy) => - f(ControllerClickEvent(receivedBy, h.model)) - case UiEvent(OnChange(_, value), receivedBy) => - val e = receivedBy match - case _: OnChangeEventHandler.CanHandleOnChangeEvent[_] => ControllerChangeEvent(receivedBy, h.model, value) - case _: OnChangeBooleanEventHandler.CanHandleOnChangeEvent[_] => ControllerChangeBooleanEvent(receivedBy, h.model, value.toBoolean) + case OnClick(key) => + f(ControllerClickEvent(componentsByKey(key), h)) + case OnChange(key, value) => + val receivedBy = componentsByKey(key) + val e = receivedBy match + case _: OnChangeEventHandler.CanHandleOnChangeEvent[_] => ControllerChangeEvent(receivedBy, h, value) + case _: OnChangeBooleanEventHandler.CanHandleOnChangeEvent[_] => ControllerChangeBooleanEvent(receivedBy, h, value.toBoolean) f(e) - case x => throw new IllegalStateException(s"Unexpected state $x") + case x => throw new IllegalStateException(s"Unexpected state $x") + + lazy val clickHandlers = clickHandlersMap(h) + lazy val changeHandlers = changeHandlersMap(h) + lazy val changeBooleanHandlers = changeBooleanHandlersMap(h) val handled = event match - case UiEvent(OnClick(key), receivedBy) if clickHandlers.contains(key) => - val handler = clickHandlers(key) - val handled = handler(ControllerClickEvent(receivedBy, h.model)) + case OnClick(key) if clickHandlers.contains(key) => + val handlers = clickHandlers(key) + val receivedBy = h.componentsByKey(key) + val handled = handlers.foldLeft(h): (handled, handler) => + handler(ControllerClickEvent(receivedBy, handled)) handled - case UiEvent(OnChange(key, value), receivedBy) if changeHandlers.contains(key) => - val handler = changeHandlers(key) - val handled = handler(ControllerChangeEvent(receivedBy, h.model, value)) + case OnChange(key, value) if changeHandlers.contains(key) => + val handlers = changeHandlers(key) + val receivedBy = h.componentsByKey(key) + val handled = handlers.foldLeft(h): (handled, handler) => + handler(ControllerChangeEvent(receivedBy, handled, value)) handled - case UiEvent(OnChange(key, value), receivedBy) if changeBooleanHandlers.contains(key) => - val handler = changeBooleanHandlers(key) - val handled = handler(ControllerChangeBooleanEvent(receivedBy, h.model, value.toBoolean)) + case OnChange(key, value) if changeBooleanHandlers.contains(key) => + val handlers = changeBooleanHandlers(key) + val receivedBy = h.componentsByKey(key) + val handled = handlers.foldLeft(h): (handled, handler) => + handler(ControllerChangeBooleanEvent(receivedBy, handled, value.toBoolean)) handled - case _ => h + case _ => h handled .tapEach: handled => renderChanges(handled.renderChanges) @@ -107,26 +98,41 @@ class Controller[M]( ) object Controller: - def apply[M](initialModel: M)(using session: ConnectedSession): Controller[M] = - new Controller(session.eventIterator, session.renderChanges, initialModel, Nil, Map.empty, Map.empty, Map.empty) + def apply[M](initialModel: Model[M], components: Seq[UiElement])(using session: ConnectedSession): Controller[M] = + new Controller(session.eventIterator, session.renderChanges, components, initialModel, Nil) trait ControllerEvent[M]: - def model: M - def handled: HandledEvent[M] = HandledEvent(model, Nil, Nil, false) + def model: M = handled.model + def handled: HandledEvent[M] -case class ControllerClickEvent[M](clicked: UiElement, model: M) extends ControllerEvent[M] -case class ControllerChangeEvent[M](changed: UiElement, model: M, newValue: String) extends ControllerEvent[M] -case class ControllerChangeBooleanEvent[M](changed: UiElement, model: M, newValue: Boolean) extends ControllerEvent[M] +case class ControllerClickEvent[M](clicked: UiElement, handled: HandledEvent[M]) extends ControllerEvent[M] +case class ControllerChangeEvent[M](changed: UiElement, handled: HandledEvent[M], newValue: String) extends ControllerEvent[M] +case class ControllerChangeBooleanEvent[M](changed: UiElement, handled: HandledEvent[M], newValue: Boolean) extends ControllerEvent[M] -case class HandledEvent[M](model: M, renderChanges: Seq[UiElement], timedRenderChanges: Seq[TimedRenderChanges], shouldTerminate: Boolean): +case class HandledEvent[M]( + model: M, + componentsByKey: Map[String, UiElement], + renderChanges: Seq[UiElement], + timedRenderChanges: Seq[TimedRenderChanges], + shouldTerminate: Boolean +): def terminate: HandledEvent[M] = copy(shouldTerminate = true) def withShouldTerminate(t: Boolean): HandledEvent[M] = copy(shouldTerminate = t) def withModel(m: M): HandledEvent[M] = copy(model = m) - def withRenderChanges(changed: UiElement*): HandledEvent[M] = copy(renderChanges = changed) + def withRenderChanges(changed: UiElement*): HandledEvent[M] = copy(renderChanges = renderChanges ++ changed) def withTimedRenderChanges(changed: TimedRenderChanges*): HandledEvent[M] = copy(timedRenderChanges = changed) def addTimedRenderChange(waitInMs: Long, renderChanges: UiElement): HandledEvent[M] = copy(timedRenderChanges = timedRenderChanges :+ TimedRenderChanges(waitInMs, renderChanges)) +type OnClickEventHandlerFunction[M] = ControllerClickEvent[M] => HandledEvent[M] +type OnChangeEventHandlerFunction[M] = ControllerChangeEvent[M] => HandledEvent[M] +type OnChangeBooleanEventHandlerFunction[M] = ControllerChangeBooleanEvent[M] => HandledEvent[M] + case class TimedRenderChanges(waitInMs: Long, renderChanges: Seq[UiElement]) object TimedRenderChanges: def apply(waitInMs: Long, renderChanges: UiElement): TimedRenderChanges = TimedRenderChanges(waitInMs, Seq(renderChanges)) + +case class Model[M](value: M): + object ClickKey extends TypedMapKey[Seq[OnClickEventHandlerFunction[M]]] + object ChangeKey extends TypedMapKey[Seq[OnChangeEventHandlerFunction[M]]] + object ChangeBooleanKey extends TypedMapKey[Seq[OnChangeBooleanEventHandlerFunction[M]]] diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/collections/TypedMap.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/collections/TypedMap.scala index 86ac5675..a375f98d 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/collections/TypedMap.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/collections/TypedMap.scala @@ -4,6 +4,7 @@ class TypedMap(val m: Map[TypedMapKey[_], Any]): def +[A](kv: (TypedMapKey[A], A)): TypedMap = new TypedMap(m + kv) def apply[A](k: TypedMapKey[A]): A = m(k).asInstanceOf[A] def getOrElse[A](k: TypedMapKey[A], default: => A) = m.getOrElse(k, default).asInstanceOf[A] + def contains[A](k: TypedMapKey[A]) = m.contains(k) object TypedMap: def empty = new TypedMap(Map.empty) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala index 7c4a992f..0e1f1333 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala @@ -1,43 +1,27 @@ package org.terminal21.client.components -import org.terminal21.client.collections.TypedMapKey import org.terminal21.client.components.UiElement.{HasDataStore, HasEventHandler} -import org.terminal21.client.model.UiEvent +import org.terminal21.client.{Model, OnChangeBooleanEventHandlerFunction, OnChangeEventHandlerFunction, OnClickEventHandlerFunction} trait EventHandler -trait OnClickEventHandler extends EventHandler: - def onClick(): Unit - object OnClickEventHandler: - object Key extends TypedMapKey[Seq[OnClickEventHandler]] trait CanHandleOnClickEvent[A <: UiElement] extends HasDataStore[A]: this: A => - def onClick(h: OnClickEventHandler): A = - val handlers = dataStore.getOrElse(Key, Nil) - store(Key, handlers :+ h) - -trait OnChangeEventHandler extends EventHandler: - def onChange(newValue: String): Unit + def onClick[M](using model: Model[M])(h: OnClickEventHandlerFunction[M]): A = + val handlers = dataStore.getOrElse(model.ClickKey, Nil) + store(model.ClickKey, handlers :+ h) object OnChangeEventHandler: - object Key extends TypedMapKey[Seq[OnChangeEventHandler]] - trait CanHandleOnChangeEvent[A <: UiElement] extends HasDataStore[A] with HasEventHandler: + trait CanHandleOnChangeEvent[A <: UiElement] extends HasDataStore[A] with HasEventHandler[A]: this: A => - def onChange(h: OnChangeEventHandler): A = - val handlers = dataStore.getOrElse(Key, Nil) - store(Key, handlers :+ h) - -trait OnChangeBooleanEventHandler extends EventHandler: - def onChange(newValue: Boolean): Unit + def onChange[M](using model: Model[M])(h: OnChangeEventHandlerFunction[M]): A = + val handlers = dataStore.getOrElse(model.ChangeKey, Nil) + store(model.ChangeKey, handlers :+ h) object OnChangeBooleanEventHandler: - object Key extends TypedMapKey[Seq[OnChangeBooleanEventHandler]] - trait CanHandleOnChangeEvent[A <: UiElement] extends HasDataStore[A] with HasEventHandler: + trait CanHandleOnChangeEvent[A <: UiElement] extends HasDataStore[A] with HasEventHandler[A]: this: A => - def onChange(h: OnChangeBooleanEventHandler): A = - val handlers = dataStore.getOrElse(Key, Nil) - store(Key, handlers :+ h) - -trait GlobalEventHandler extends EventHandler: - def onEvent(event: UiEvent): Unit + def onChange[M](using model: Model[M])(h: OnChangeBooleanEventHandlerFunction[M]): A = + val handlers = dataStore.getOrElse(model.ChangeBooleanKey, Nil) + store(model.ChangeBooleanKey, handlers :+ h) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala index 44c7b279..c2ab91b8 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala @@ -1,65 +1,65 @@ -package org.terminal21.client.components - -import functions.fibers.FiberExecutor -import org.terminal21.client.ConnectedSession -import org.terminal21.client.components.UiElement.HasStyle -import org.terminal21.client.components.chakra.* - -import java.util.concurrent.atomic.{AtomicBoolean, AtomicReference} - -/** Creates a standard UI for a calculation which may take time. While the calculation runs, the UI is grayed out, including the dataUi component. When the - * calculation completes, it allows for updating the dataUi component. - * @tparam OUT - * the return value of the calculation. - */ -trait StdUiCalculation[OUT]( - name: String, - dataUi: UiElement with HasStyle[_] -)(using session: ConnectedSession, executor: FiberExecutor) - extends Calculation[OUT] - with UiComponent: - private val running = new AtomicBoolean(false) - private val currentUi = new AtomicReference(dataUi) - - protected def updateUi(dataUi: UiElement & HasStyle[_]) = currentUi.set(dataUi) - - lazy val badge = Badge() - lazy val recalc = Button(text = "Recalculate", size = Some("sm"), leftIcon = Some(RepeatIcon())).onClick: () => - if running.compareAndSet(false, true) then - try - reCalculate() - finally running.set(false) - - override lazy val rendered: Seq[UiElement] = - val header = Box( - bg = "green", - p = 4, - children = Seq( - HStack(children = Seq(Text(text = name), badge, recalc)) - ) - ) - Seq(header, dataUi) - - override def onError(t: Throwable): Unit = - session.renderChanges( - badge.withText(s"Error: ${t.getMessage}").withColorScheme(Some("red")), - dataUi, - recalc.withIsDisabled(None) - ) - super.onError(t) - - override protected def whenResultsNotReady(): Unit = - session.renderChanges( - badge.withText("Calculating").withColorScheme(Some("purple")), - currentUi.get().withStyle(dataUi.style + ("filter" -> "grayscale(100%)")), - recalc.withIsDisabled(Some(true)) - ) - super.whenResultsNotReady() - - override protected def whenResultsReady(results: OUT): Unit = - val newDataUi = currentUi.get().withStyle(dataUi.style - "filter") - session.renderChanges( - badge.withText("Ready").withColorScheme(None), - newDataUi, - recalc.withIsDisabled(Some(false)) - ) +//package org.terminal21.client.components +// +//import functions.fibers.FiberExecutor +//import org.terminal21.client.ConnectedSession +//import org.terminal21.client.components.UiElement.HasStyle +//import org.terminal21.client.components.chakra.* +// +//import java.util.concurrent.atomic.{AtomicBoolean, AtomicReference} +// +///** Creates a standard UI for a calculation which may take time. While the calculation runs, the UI is grayed out, including the dataUi component. When the +// * calculation completes, it allows for updating the dataUi component. +// * @tparam OUT +// * the return value of the calculation. +// */ +//trait StdUiCalculation[OUT]( +// name: String, +// dataUi: UiElement with HasStyle[_] +//)(using session: ConnectedSession, executor: FiberExecutor) +// extends Calculation[OUT] +// with UiComponent: +// private val running = new AtomicBoolean(false) +// private val currentUi = new AtomicReference(dataUi) +// +// protected def updateUi(dataUi: UiElement & HasStyle[_]) = currentUi.set(dataUi) +// +// lazy val badge = Badge() +// lazy val recalc = Button(text = "Recalculate", size = Some("sm"), leftIcon = Some(RepeatIcon())).onClick: () => +// if running.compareAndSet(false, true) then +// try +// reCalculate() +// finally running.set(false) +// +// override lazy val rendered: Seq[UiElement] = +// val header = Box( +// bg = "green", +// p = 4, +// children = Seq( +// HStack(children = Seq(Text(text = name), badge, recalc)) +// ) +// ) +// Seq(header, dataUi) +// +// override def onError(t: Throwable): Unit = +// session.renderChanges( +// badge.withText(s"Error: ${t.getMessage}").withColorScheme(Some("red")), +// dataUi, +// recalc.withIsDisabled(None) +// ) +// super.onError(t) +// +// override protected def whenResultsNotReady(): Unit = +// session.renderChanges( +// badge.withText("Calculating").withColorScheme(Some("purple")), +// currentUi.get().withStyle(dataUi.style + ("filter" -> "grayscale(100%)")), +// recalc.withIsDisabled(Some(true)) +// ) +// super.whenResultsNotReady() +// +// override protected def whenResultsReady(results: OUT): Unit = +// val newDataUi = currentUi.get().withStyle(dataUi.style - "filter") +// session.renderChanges( +// badge.withText("Ready").withColorScheme(None), +// newDataUi, +// recalc.withIsDisabled(Some(false)) +// ) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala index 4b56db1d..1bdb613c 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala @@ -11,18 +11,10 @@ trait UiElement extends AnyElement: */ def flat: Seq[UiElement] = Seq(this) - def render()(using session: ConnectedSession): Unit = - session.render(this) - - /** Renders any changes for this element and it's children (if any). The element must previously have been added to the session. - */ - def renderChanges()(using session: ConnectedSession): Unit = - session.renderChanges(this) - object UiElement: trait Current[A <: UiElement]: this: UiElement => - def current(using session: ConnectedSession): A = session.currentState(this.asInstanceOf[A]) + def current: A = ??? trait HasChildren[A <: UiElement]: this: A => @@ -32,8 +24,8 @@ object UiElement: def noChildren: A = withChildren() def addChildren(cn: UiElement*): A = withChildren(children ++ cn: _*) - trait HasEventHandler: - def defaultEventHandler(session: ConnectedSession): EventHandler + trait HasEventHandler[A]: + def defaultEventHandler: String => A trait HasStyle[A <: UiElement]: def style: Map[String, Any] diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala index 2b30b513..fb93d5a3 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala @@ -1,9 +1,9 @@ package org.terminal21.client.components.chakra +import org.terminal21.client.ConnectedSession import org.terminal21.client.collections.TypedMap import org.terminal21.client.components.UiElement.{Current, HasChildren, HasEventHandler, HasStyle} -import org.terminal21.client.components.{Keys, OnChangeBooleanEventHandler, OnChangeEventHandler, OnClickEventHandler, UiElement} -import org.terminal21.client.ConnectedSession +import org.terminal21.client.components.* sealed trait CEJson extends UiElement @@ -157,17 +157,16 @@ case class Editable( children: Seq[UiElement] = Nil, dataStore: TypedMap = TypedMap.empty ) extends ChakraElement[Editable] - with HasEventHandler with HasChildren[Editable] with OnChangeEventHandler.CanHandleOnChangeEvent[Editable]: - override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = - newValue => session.modified(copy(valueReceived = Some(newValue))) - override def withChildren(cn: UiElement*) = copy(children = cn) - override def withStyle(v: Map[String, Any]) = copy(style = v) - def withKey(v: String) = copy(key = v) - def withDefaultValue(v: String) = copy(defaultValue = v) - def value = valueReceived.getOrElse(defaultValue) - override def withDataStore(ds: TypedMap): Editable = copy(dataStore = ds) + override def defaultEventHandler = + newValue => copy(valueReceived = Some(newValue)) + override def withChildren(cn: UiElement*) = copy(children = cn) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withDefaultValue(v: String) = copy(defaultValue = v) + def value = valueReceived.getOrElse(defaultValue) + override def withDataStore(ds: TypedMap): Editable = copy(dataStore = ds) case class EditablePreview(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty) extends ChakraElement[EditablePreview]: override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -236,18 +235,17 @@ case class Input( style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty ) extends ChakraElement[Input] - with HasEventHandler with OnChangeEventHandler.CanHandleOnChangeEvent[Input]: - override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = newValue => session.modified(copy(valueReceived = Some(newValue))) - override def withStyle(v: Map[String, Any]): Input = copy(style = v) - def withKey(v: String): Input = copy(key = v) - def withType(v: String): Input = copy(`type` = v) - def withPlaceholder(v: String): Input = copy(placeholder = v) - def withSize(v: String): Input = copy(size = v) - def withVariant(v: Option[String]): Input = copy(variant = v) - def withDefaultValue(v: String): Input = copy(defaultValue = v) - def value: String = valueReceived.getOrElse(defaultValue) - override def withDataStore(ds: TypedMap): Input = copy(dataStore = ds) + override def defaultEventHandler = newValue => copy(valueReceived = Some(newValue)) + override def withStyle(v: Map[String, Any]): Input = copy(style = v) + def withKey(v: String): Input = copy(key = v) + def withType(v: String): Input = copy(`type` = v) + def withPlaceholder(v: String): Input = copy(placeholder = v) + def withSize(v: String): Input = copy(size = v) + def withVariant(v: Option[String]): Input = copy(variant = v) + def withDefaultValue(v: String): Input = copy(defaultValue = v) + def value: String = valueReceived.getOrElse(defaultValue) + override def withDataStore(ds: TypedMap): Input = copy(dataStore = ds) case class InputGroup( key: String = Keys.nextKey, @@ -296,16 +294,15 @@ case class Checkbox( checkedV: Option[Boolean] = None, dataStore: TypedMap = TypedMap.empty ) extends ChakraElement[Checkbox] - with HasEventHandler with OnChangeBooleanEventHandler.CanHandleOnChangeEvent[Checkbox]: - def checked: Boolean = checkedV.getOrElse(defaultChecked) - override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = newValue => session.modified(copy(checkedV = Some(newValue.toBoolean))) - override def withStyle(v: Map[String, Any]) = copy(style = v) - def withKey(v: String) = copy(key = v) - def withText(v: String) = copy(text = v) - def withDefaultChecked(v: Boolean) = copy(defaultChecked = v) - def withIsDisabled(v: Boolean) = copy(isDisabled = v) - override def withDataStore(ds: TypedMap): Checkbox = copy(dataStore = ds) + def checked: Boolean = checkedV.getOrElse(defaultChecked) + override def defaultEventHandler = newValue => copy(checkedV = Some(newValue.toBoolean)) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withText(v: String) = copy(text = v) + def withDefaultChecked(v: Boolean) = copy(defaultChecked = v) + def withIsDisabled(v: Boolean) = copy(isDisabled = v) + override def withDataStore(ds: TypedMap): Checkbox = copy(dataStore = ds) /** https://chakra-ui.com/docs/components/radio */ @@ -330,16 +327,15 @@ case class RadioGroup( children: Seq[UiElement] = Nil, dataStore: TypedMap = TypedMap.empty ) extends ChakraElement[RadioGroup] - with HasEventHandler with HasChildren[RadioGroup] with OnChangeEventHandler.CanHandleOnChangeEvent[RadioGroup]: - override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = newValue => session.modified(copy(valueReceived = Some(newValue))) - override def withChildren(cn: UiElement*) = copy(children = cn) - override def withStyle(v: Map[String, Any]) = copy(style = v) - def value: String = valueReceived.getOrElse(defaultValue) - def withKey(v: String) = copy(key = v) - def withDefaultValue(v: String) = copy(defaultValue = v) - override def withDataStore(ds: TypedMap): RadioGroup = copy(dataStore = ds) + override def defaultEventHandler = newValue => copy(valueReceived = Some(newValue)) + override def withChildren(cn: UiElement*) = copy(children = cn) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def value: String = valueReceived.getOrElse(defaultValue) + def withKey(v: String) = copy(key = v) + def withDefaultValue(v: String) = copy(defaultValue = v) + override def withDataStore(ds: TypedMap): RadioGroup = copy(dataStore = ds) case class Center( key: String = Keys.nextKey, @@ -1402,18 +1398,17 @@ case class Textarea( style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty ) extends ChakraElement[Textarea] - with HasEventHandler with OnChangeEventHandler.CanHandleOnChangeEvent[Textarea]: - override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = newValue => session.modified(copy(valueReceived = Some(newValue))) - override def withStyle(v: Map[String, Any]) = copy(style = v) - def withKey(v: String) = copy(key = v) - def withType(v: String) = copy(`type` = v) - def withPlaceholder(v: String) = copy(placeholder = v) - def withSize(v: String) = copy(size = v) - def withVariant(v: Option[String]) = copy(variant = v) - def withDefaultValue(v: String) = copy(defaultValue = v) - def value = valueReceived.getOrElse(defaultValue) - override def withDataStore(ds: TypedMap): Textarea = copy(dataStore = ds) + override def defaultEventHandler = newValue => copy(valueReceived = Some(newValue)) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withType(v: String) = copy(`type` = v) + def withPlaceholder(v: String) = copy(placeholder = v) + def withSize(v: String) = copy(size = v) + def withVariant(v: Option[String]) = copy(variant = v) + def withDefaultValue(v: String) = copy(defaultValue = v) + def value = valueReceived.getOrElse(defaultValue) + override def withDataStore(ds: TypedMap): Textarea = copy(dataStore = ds) /** https://chakra-ui.com/docs/components/switch */ @@ -1426,16 +1421,15 @@ case class Switch( checkedV: Option[Boolean] = None, // use checked dataStore: TypedMap = TypedMap.empty ) extends ChakraElement[Switch] - with HasEventHandler with OnChangeBooleanEventHandler.CanHandleOnChangeEvent[Switch]: - def checked: Boolean = checkedV.getOrElse(defaultChecked) - override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = newValue => session.modified(copy(checkedV = Some(newValue.toBoolean))) - override def withStyle(v: Map[String, Any]) = copy(style = v) - def withKey(v: String) = copy(key = v) - def withText(v: String) = copy(text = v) - def withDefaultChecked(v: Boolean) = copy(defaultChecked = v) - def withIsDisabled(v: Boolean) = copy(isDisabled = v) - override def withDataStore(ds: TypedMap): Switch = copy(dataStore = ds) + def checked: Boolean = checkedV.getOrElse(defaultChecked) + override def defaultEventHandler = newValue => copy(checkedV = Some(newValue.toBoolean)) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withText(v: String) = copy(text = v) + def withDefaultChecked(v: Boolean) = copy(defaultChecked = v) + def withIsDisabled(v: Boolean) = copy(isDisabled = v) + override def withDataStore(ds: TypedMap): Switch = copy(dataStore = ds) /** https://chakra-ui.com/docs/components/select */ @@ -1451,20 +1445,19 @@ case class Select( children: Seq[UiElement] = Nil, dataStore: TypedMap = TypedMap.empty ) extends ChakraElement[Select] - with HasEventHandler with HasChildren[Select] with OnChangeEventHandler.CanHandleOnChangeEvent[Select]: - override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = newValue => session.modified(copy(valueReceived = Some(newValue))) - override def withStyle(v: Map[String, Any]) = copy(style = v) - override def withChildren(cn: UiElement*) = copy(children = cn) - def withKey(v: String) = copy(key = v) - def withPlaceholder(v: String) = copy(placeholder = v) - def withDefaultValue(v: String) = copy(defaultValue = v) - def withBg(v: Option[String]) = copy(bg = v) - def withColor(v: Option[String]) = copy(color = v) - def withBorderColor(v: Option[String]) = copy(borderColor = v) - def value = valueReceived.getOrElse(defaultValue) - override def withDataStore(ds: TypedMap): Select = copy(dataStore = ds) + override def defaultEventHandler = newValue => copy(valueReceived = Some(newValue)) + override def withStyle(v: Map[String, Any]) = copy(style = v) + override def withChildren(cn: UiElement*) = copy(children = cn) + def withKey(v: String) = copy(key = v) + def withPlaceholder(v: String) = copy(placeholder = v) + def withDefaultValue(v: String) = copy(defaultValue = v) + def withBg(v: Option[String]) = copy(bg = v) + def withColor(v: Option[String]) = copy(color = v) + def withBorderColor(v: Option[String]) = copy(borderColor = v) + def value = valueReceived.getOrElse(defaultValue) + override def withDataStore(ds: TypedMap): Select = copy(dataStore = ds) case class Option_( key: String = Keys.nextKey, diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/extensions.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/extensions.scala deleted file mode 100644 index 003d52ea..00000000 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/extensions.scala +++ /dev/null @@ -1,10 +0,0 @@ -package org.terminal21.client.components - -import org.terminal21.client.ConnectedSession - -extension (s: Seq[UiElement]) - def render()(using session: ConnectedSession): Unit = - session.render(s*) - - def renderChanges()(using session: ConnectedSession): Unit = - session.renderChanges(s*) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala index be50fc62..eceb080d 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala @@ -73,12 +73,11 @@ case class Input( valueReceived: Option[String] = None, // use value instead dataStore: TypedMap = TypedMap.empty ) extends StdElement[Input] - with HasEventHandler with CanHandleOnChangeEvent[Input]: - override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = newValue => session.modified(copy(valueReceived = Some(newValue))) - override def withStyle(v: Map[String, Any]) = copy(style = v) - def withKey(v: String) = copy(key = v) - def withType(v: String) = copy(`type` = v) - def withDefaultValue(v: String) = copy(defaultValue = v) - def value = valueReceived.getOrElse(defaultValue) - override def withDataStore(ds: TypedMap): Input = copy(dataStore = ds) + override def defaultEventHandler = newValue => copy(valueReceived = Some(newValue)) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withType(v: String) = copy(`type` = v) + def withDefaultValue(v: String) = copy(defaultValue = v) + def value = valueReceived.getOrElse(defaultValue) + override def withDataStore(ds: TypedMap): Input = copy(dataStore = ds) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala index 0997c75f..ccd19b53 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala @@ -43,7 +43,6 @@ case class CookieReader( requestId: String = TransientRequest.newRequestId(), dataStore: TypedMap = TypedMap.empty ) extends StdHttp - with HasEventHandler with CanHandleOnChangeEvent[CookieReader]: - override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = newValue => session.modified(copy(value = Some(newValue))) - override def withDataStore(ds: TypedMap): CookieReader = copy(dataStore = ds) + override def defaultEventHandler = newValue => copy(value = Some(newValue)) + override def withDataStore(ds: TypedMap): CookieReader = copy(dataStore = ds) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/model/GlobalEvent.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/model/GlobalEvent.scala deleted file mode 100644 index 1bdd0330..00000000 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/model/GlobalEvent.scala +++ /dev/null @@ -1,24 +0,0 @@ -package org.terminal21.client.model - -import org.terminal21.client.components.UiElement -import org.terminal21.model.{CommandEvent, OnChange, OnClick} - -/** These are the events as handled by the std lib. They enrich the server events with local information. - */ -sealed trait GlobalEvent: - def isTarget(e: UiElement): Boolean - def isSessionClose: Boolean - -object GlobalEvent: - def onClick(receivedBy: UiElement): UiEvent = UiEvent(OnClick(receivedBy.key), receivedBy) - def onChange(receivedBy: UiElement, value: String): UiEvent = UiEvent(OnChange(receivedBy.key, value), receivedBy) - def onChange(receivedBy: UiElement, value: Boolean): UiEvent = UiEvent(OnChange(receivedBy.key, value.toString), receivedBy) - def sessionClosed: GlobalEvent = SessionClosedEvent - -case class UiEvent(event: CommandEvent, receivedBy: UiElement) extends GlobalEvent: - override def isTarget(e: UiElement): Boolean = e.key == receivedBy.key - override def isSessionClose: Boolean = false - -case object SessionClosedEvent extends GlobalEvent: - override def isTarget(e: UiElement): Boolean = false - override def isSessionClose: Boolean = true diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/CalculationTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/CalculationTest.scala index 9fea22ac..81e38136 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/CalculationTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/CalculationTest.scala @@ -1,37 +1,37 @@ -package org.terminal21.client - -import functions.fibers.FiberExecutor -import org.scalatest.funsuite.AnyFunSuiteLike -import org.scalatest.matchers.should.Matchers.* - -import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger} -import org.scalatest.concurrent.Eventually.* -import org.terminal21.client.components.Calculation - -class CalculationTest extends AnyFunSuiteLike: - given executor: FiberExecutor = FiberExecutor() - def testCalc(i: Int) = i + 1 - def testCalcString(i: Int): String = (i + 10).toString - - class Calc extends Calculation[Int]: - val whenResultsNotReadyCalled = new AtomicBoolean(false) - val whenResultsReadyValue = new AtomicInteger(-1) - override protected def whenResultsNotReady(): Unit = whenResultsNotReadyCalled.set(true) - override protected def whenResultsReady(results: Int): Unit = whenResultsReadyValue.set(results) - override protected def calculation() = 2 - - test("calculates"): - val calc = new Calc - calc.run().get() should be(2) - - test("calls whenResultsNotReady"): - val calc = new Calc - calc.run() - eventually: - calc.whenResultsNotReadyCalled.get() should be(true) - - test("calls whenResultsReady"): - val calc = new Calc - calc.run() - eventually: - calc.whenResultsReadyValue.get() should be(2) +//package org.terminal21.client +// +//import functions.fibers.FiberExecutor +//import org.scalatest.funsuite.AnyFunSuiteLike +//import org.scalatest.matchers.should.Matchers.* +// +//import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger} +//import org.scalatest.concurrent.Eventually.* +//import org.terminal21.client.components.Calculation +// +//class CalculationTest extends AnyFunSuiteLike: +// given executor: FiberExecutor = FiberExecutor() +// def testCalc(i: Int) = i + 1 +// def testCalcString(i: Int): String = (i + 10).toString +// +// class Calc extends Calculation[Int]: +// val whenResultsNotReadyCalled = new AtomicBoolean(false) +// val whenResultsReadyValue = new AtomicInteger(-1) +// override protected def whenResultsNotReady(): Unit = whenResultsNotReadyCalled.set(true) +// override protected def whenResultsReady(results: Int): Unit = whenResultsReadyValue.set(results) +// override protected def calculation() = 2 +// +// test("calculates"): +// val calc = new Calc +// calc.run().get() should be(2) +// +// test("calls whenResultsNotReady"): +// val calc = new Calc +// calc.run() +// eventually: +// calc.whenResultsNotReadyCalled.get() should be(true) +// +// test("calls whenResultsReady"): +// val calc = new Calc +// calc.run() +// eventually: +// calc.whenResultsReadyValue.get() should be(2) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala index 289c6c50..afa11a28 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala @@ -7,18 +7,14 @@ import org.scalatest.matchers.should.Matchers.* import org.terminal21.client.ConnectedSessionMock.encoder import org.terminal21.client.components.chakra.{Button, Checkbox, Editable, Input} import org.terminal21.client.components.std.{Paragraph, Span} -import org.terminal21.client.model.UiEvent import org.terminal21.model.{CommandEvent, OnChange} import org.terminal21.ui.std.ServerJson -import java.util.concurrent.atomic.AtomicBoolean - class ConnectedSessionTest extends AnyFunSuiteLike: - test("global event iterator"): + test("event iterator"): given connectedSession: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock val editable = Editable() - editable.render() val it = connectedSession.eventIterator val event1 = OnChange(editable.key, "v1") val event2 = OnChange(editable.key, "v2") @@ -27,54 +23,18 @@ class ConnectedSessionTest extends AnyFunSuiteLike: connectedSession.clear() it.toList should be( List( - UiEvent(event1, editable.copy(valueReceived = Some("v1"))), - UiEvent(event2, editable.copy(valueReceived = Some("v2"))) + event1, + event2 ) ) - test("click events are processed by onClick handlers"): - given connectedSession: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock - var clicked = false - val button = Button().onClick: () => - clicked = true - connectedSession.render(button) - connectedSession.fireEvent(CommandEvent.onClick(button)) - clicked should be(true) - - test("change events are processed by onChange handlers"): - given connectedSession: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock - var value = "" - val input = Input().onChange: newValue => - value = newValue - connectedSession.render(input) - connectedSession.fireEvent(CommandEvent.onChange(input, "new-value")) - value should be("new-value") - - test("change boolean events are processed by onChange handlers"): - given connectedSession: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock - var value = false - val checkbox = Checkbox().onChange: newValue => - value = newValue - connectedSession.render(checkbox) - connectedSession.fireEvent(CommandEvent.onChange(checkbox, true)) - value should be(true) - - test("default event handlers are invoked before user handlers"): - given connectedSession: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock - val editableI = Editable() - val editable = editableI.onChange: newValue => - editableI.current.value should be(newValue) - - connectedSession.render(editable) - connectedSession.fireEvent(OnChange(editable.key, "new value")) - test("to server json"): val (sessionService, connectedSession) = ConnectedSessionMock.newConnectedSessionAndSessionServiceMock val p1 = Paragraph(text = "p1") val span1 = Span(text = "span1") - connectedSession.render(p1.withChildren(span1)) - connectedSession.render() + connectedSession.render(Seq(p1.withChildren(span1))) + connectedSession.render(Nil) verify(sessionService).setSessionJsonState( connectedSession.session, ServerJson( @@ -89,8 +49,8 @@ class ConnectedSessionTest extends AnyFunSuiteLike: val p1 = Paragraph(text = "p1") val span1 = Span(text = "span1") - connectedSession.render(p1) - connectedSession.renderChanges(p1.withChildren(span1)) + connectedSession.render(Seq(p1)) + connectedSession.renderChanges(Seq(p1.withChildren(span1))) verify(sessionService).changeSessionJsonState( connectedSession.session, ServerJson( @@ -105,8 +65,8 @@ class ConnectedSessionTest extends AnyFunSuiteLike: val p1 = Paragraph(text = "p1") val span1 = Span(text = "span1") - connectedSession.render(p1) - connectedSession.renderChanges(p1.withChildren(span1)) + connectedSession.render(Seq(p1)) + connectedSession.renderChanges(Seq(p1.withChildren(span1))) p1.current.children should be(Seq(span1)) test("renderChanges updates current version of component when component deeply nested"): @@ -114,6 +74,6 @@ class ConnectedSessionTest extends AnyFunSuiteLike: val span1 = Span(text = "span1") val p1 = Paragraph(text = "p1").withChildren(span1) - connectedSession.render(p1) - connectedSession.renderChanges(p1.withChildren(span1.withText("span-text-changed"))) + connectedSession.render(Seq(p1)) + connectedSession.renderChanges(Seq(p1.withChildren(span1.withText("span-text-changed")))) span1.current.text should be("span-text-changed") diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 06baac27..5c3b3799 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -5,66 +5,86 @@ import org.scalatest.matchers.should.Matchers.* import org.terminal21.client.components.UiElement import org.terminal21.client.components.chakra.{Button, Checkbox} import org.terminal21.client.components.std.Input -import org.terminal21.client.model.{GlobalEvent, UiEvent} -import org.terminal21.model.{OnChange, OnClick} +import org.terminal21.model.{CommandEvent, OnChange, OnClick} class ControllerTest extends AnyFunSuiteLike: val button = Button() - val buttonClick = UiEvent(OnClick(button.key), button) + val buttonClick = OnClick(button.key) val input = Input() - val inputChange = UiEvent(OnChange(input.key, "new-value"), input) + val inputChange = OnChange(input.key, "new-value") val checkbox = Checkbox() - val checkBoxChange = UiEvent(OnChange(checkbox.key, "true"), checkbox) + val checkBoxChange = OnChange(checkbox.key, "true") - def newController[M](initialModel: M, eventIterator: => Iterator[GlobalEvent], renderChanges: Seq[UiElement] => Unit = _ => ()): Controller[M] = - new Controller(eventIterator, renderChanges, initialModel, Nil, Map.empty, Map.empty, Map.empty) + def newController[M]( + initialModel: Model[M], + eventIterator: => Iterator[CommandEvent], + components: Seq[UiElement], + renderChanges: Seq[UiElement] => Unit = _ => () + ): Controller[M] = + new Controller(eventIterator, renderChanges, components, initialModel, Nil) test("onEvent is called"): - newController(0, Iterator(buttonClick)) + val model = Model(0) + newController(model, Iterator(buttonClick), Seq(button)) .onEvent: event => if event.model > 1 then event.handled.terminate else event.handled.withModel(event.model + 1) .eventsIterator .toList should be(List(0, 1)) test("onEvent is called for change"): - newController(0, Iterator(inputChange)) + val model = Model(0) + newController(model, Iterator(inputChange), Seq(input)) .onEvent: - case event @ ControllerChangeEvent(`input`, 0, "new-value") => - if event.model > 1 then event.handled.terminate else event.handled.withModel(event.model + 1) + case event @ ControllerChangeEvent(`input`, handled, "new-value") => + if event.model > 1 then handled.terminate else handled.withModel(event.model + 1) .eventsIterator .toList should be(List(0, 1)) test("onEvent is called for change/boolean"): - newController(0, Iterator(checkBoxChange)) + val model = Model(0) + newController(model, Iterator(checkBoxChange), Seq(checkbox)) .onEvent: - case event @ ControllerChangeBooleanEvent(`checkbox`, 0, true) => - if event.model > 1 then event.handled.terminate else event.handled.withModel(event.model + 1) + case event @ ControllerChangeBooleanEvent(`checkbox`, handled, true) => + if event.model > 1 then handled.terminate else handled.withModel(event.model + 1) .eventsIterator .toList should be(List(0, 1)) test("onClick is called"): - newController(0, Iterator(buttonClick)) - .onClick(button): event => - event.handled.withModel(100).terminate - .eventsIterator - .toList should be(List(0, 100)) + given model: Model[Int] = Model(0) + newController( + model, + Iterator(buttonClick), + Seq( + button.onClick: event => + event.handled.withModel(100).terminate + ) + ).eventsIterator.toList should be(List(0, 100)) test("onChange is called"): - newController(0, Iterator(inputChange)) - .onChange(input): event => - event.handled.withModel(100).terminate - .eventsIterator - .toList should be(List(0, 100)) + given model: Model[Int] = Model(0) + newController( + model, + Iterator(inputChange), + Seq( + input.onChange: event => + event.handled.withModel(100).terminate + ) + ).eventsIterator.toList should be(List(0, 100)) test("onChange/boolean is called"): - newController(0, Iterator(checkBoxChange)) - .onChange(checkbox): event => - event.handled.withModel(100).terminate - .eventsIterator - .toList should be(List(0, 100)) + given model: Model[Int] = Model(0) + newController( + model, + Iterator(checkBoxChange), + Seq( + checkbox.onChange: event => + event.handled.withModel(100).terminate + ) + ).eventsIterator.toList should be(List(0, 100)) test("terminate is obeyed and latest model state is iterated"): - newController(0, Iterator(buttonClick, buttonClick, buttonClick)) + val model = Model(0) + newController(model, Iterator(buttonClick, buttonClick, buttonClick), Seq(button)) .onEvent: event => if event.model > 1 then event.handled.terminate.withModel(100) else event.handled.withModel(event.model + 1) .eventsIterator @@ -74,7 +94,7 @@ class ControllerTest extends AnyFunSuiteLike: var rendered = Seq.empty[UiElement] def renderer(s: Seq[UiElement]): Unit = rendered = s - newController(0, Iterator(buttonClick), renderer) + newController(Model(0), Iterator(buttonClick), Seq(button), renderer) .onEvent: event => event.handled.withModel(event.model + 1).withRenderChanges(button.withText("changed")).terminate .eventsIterator @@ -85,7 +105,7 @@ class ControllerTest extends AnyFunSuiteLike: test("timed changes are rendered"): @volatile var rendered = Seq.empty[UiElement] def renderer(s: Seq[UiElement]): Unit = rendered = s - newController(0, Iterator(buttonClick), renderer) + newController(Model(0), Iterator(buttonClick), Seq(button), renderer) .onEvent: event => event.handled.withModel(event.model + 1).withTimedRenderChanges(TimedRenderChanges(10, button.withText("changed"))).terminate .eventsIterator diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/collections/TypedMapTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/collections/TypedMapTest.scala index 5f073689..e33cd291 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/collections/TypedMapTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/collections/TypedMapTest.scala @@ -17,3 +17,9 @@ class TypedMapTest extends AnyFunSuiteLike: test("getOrElse when key available"): (TypedMap.empty + (IntKey -> 5)).getOrElse(IntKey, 2) should be(5) + + test("contains key positive"): + (TypedMap.empty + (IntKey -> 5)).contains(IntKey) should be(true) + + test("contains key negative"): + TypedMap.empty.contains(IntKey) should be(false) From 53eb878bb2ff9dc4db35c3b8cae934a87d9a918d Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 22 Feb 2024 22:49:19 +0000 Subject: [PATCH 132/313] - --- .../org/terminal21/client/Controller.scala | 14 ++++++++++++-- .../client/components/UiElement.scala | 8 ++------ .../components/chakra/ChakraElement.scala | 4 ++-- .../client/components/std/StdElement.scala | 4 ++-- .../client/ConnectedSessionTest.scala | 18 ------------------ .../org/terminal21/client/ControllerTest.scala | 12 ++++++++++++ 6 files changed, 30 insertions(+), 30 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index ec65d298..62db7019 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -3,6 +3,7 @@ package org.terminal21.client import org.terminal21.client.collections.{EventIterator, TypedMapKey} import org.terminal21.client.components.OnChangeEventHandler.CanHandleOnChangeEvent import org.terminal21.client.components.OnClickEventHandler.CanHandleOnClickEvent +import org.terminal21.client.components.UiElement.HasEventHandler import org.terminal21.client.components.{OnChangeBooleanEventHandler, OnChangeEventHandler, OnClickEventHandler, UiElement} import org.terminal21.model.{CommandEvent, OnChange, OnClick} @@ -49,7 +50,15 @@ class Controller[M]( eventIteratorFactory .takeWhile(!_.isSessionClosed) .scanLeft(HandledEvent(initialModel.value, componentsByKey, Nil, Nil, false)): (oldHandled, event) => - val h = eventHandlers.foldLeft(oldHandled.copy(renderChanges = Nil, timedRenderChanges = Nil)): (h, f) => + val initHandled = oldHandled.componentsByKey(event.key) match + case e: UiElement with HasEventHandler[_] => + event match + case OnChange(key, value) => + oldHandled.copy(componentsByKey = oldHandled.componentsByKey + (key -> e.defaultEventHandler(value))) + case _ => oldHandled + case _ => oldHandled + + val h = eventHandlers.foldLeft(initHandled.copy(renderChanges = Nil, timedRenderChanges = Nil)): (h, f) => event match case OnClick(key) => f(ControllerClickEvent(componentsByKey(key), h)) @@ -102,8 +111,9 @@ object Controller: new Controller(session.eventIterator, session.renderChanges, components, initialModel, Nil) trait ControllerEvent[M]: - def model: M = handled.model + def model: M = handled.model def handled: HandledEvent[M] + extension [A <: UiElement](e: UiElement with HasEventHandler[A]) def current: A = handled.componentsByKey(e.key).asInstanceOf[A] case class ControllerClickEvent[M](clicked: UiElement, handled: HandledEvent[M]) extends ControllerEvent[M] case class ControllerChangeEvent[M](changed: UiElement, handled: HandledEvent[M], newValue: String) extends ControllerEvent[M] diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala index 1bdb613c..96c6a984 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala @@ -1,7 +1,7 @@ package org.terminal21.client.components +import org.terminal21.client.HandledEvent import org.terminal21.client.collections.{TypedMap, TypedMapKey} -import org.terminal21.client.ConnectedSession trait UiElement extends AnyElement: def key: String @@ -12,10 +12,6 @@ trait UiElement extends AnyElement: def flat: Seq[UiElement] = Seq(this) object UiElement: - trait Current[A <: UiElement]: - this: UiElement => - def current: A = ??? - trait HasChildren[A <: UiElement]: this: A => def children: Seq[UiElement] @@ -24,7 +20,7 @@ object UiElement: def noChildren: A = withChildren() def addChildren(cn: UiElement*): A = withChildren(children ++ cn: _*) - trait HasEventHandler[A]: + trait HasEventHandler[A <: UiElement]: def defaultEventHandler: String => A trait HasStyle[A <: UiElement]: diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala index fb93d5a3..dac20548 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala @@ -2,7 +2,7 @@ package org.terminal21.client.components.chakra import org.terminal21.client.ConnectedSession import org.terminal21.client.collections.TypedMap -import org.terminal21.client.components.UiElement.{Current, HasChildren, HasEventHandler, HasStyle} +import org.terminal21.client.components.UiElement.{HasChildren, HasEventHandler, HasStyle} import org.terminal21.client.components.* sealed trait CEJson extends UiElement @@ -11,7 +11,7 @@ sealed trait CEJson extends UiElement * https://github.com/kostaskougios/terminal21-restapi/blob/main/examples/src/main/scala/tests/ChakraComponents.scala and it's related scala files under * https://github.com/kostaskougios/terminal21-restapi/tree/main/examples/src/main/scala/tests/chakra */ -sealed trait ChakraElement[A <: ChakraElement[A]] extends CEJson with HasStyle[A] with Current[A] +sealed trait ChakraElement[A <: ChakraElement[A]] extends CEJson with HasStyle[A] /** https://chakra-ui.com/docs/components/button */ diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala index eceb080d..b689d7c1 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala @@ -2,12 +2,12 @@ package org.terminal21.client.components.std import org.terminal21.client.components.OnChangeEventHandler.CanHandleOnChangeEvent import org.terminal21.client.collections.TypedMap -import org.terminal21.client.components.UiElement.{Current, HasChildren, HasEventHandler, HasStyle} +import org.terminal21.client.components.UiElement.{HasChildren, HasEventHandler, HasStyle} import org.terminal21.client.components.{Keys, OnChangeEventHandler, UiElement} import org.terminal21.client.ConnectedSession sealed trait StdEJson extends UiElement -sealed trait StdElement[A <: UiElement] extends StdEJson with HasStyle[A] with Current[A] +sealed trait StdElement[A <: UiElement] extends StdEJson with HasStyle[A] case class Span(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement[Span]: override def withStyle(v: Map[String, Any]) = copy(style = v) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala index afa11a28..7e271748 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala @@ -59,21 +59,3 @@ class ConnectedSessionTest extends AnyFunSuiteLike: Map(p1.key -> Seq(span1.key), span1.key -> Nil) ) ) - - test("renderChanges updates current version of component"): - given connectedSession: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock - - val p1 = Paragraph(text = "p1") - val span1 = Span(text = "span1") - connectedSession.render(Seq(p1)) - connectedSession.renderChanges(Seq(p1.withChildren(span1))) - p1.current.children should be(Seq(span1)) - - test("renderChanges updates current version of component when component deeply nested"): - given connectedSession: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock - - val span1 = Span(text = "span1") - val p1 = Paragraph(text = "p1").withChildren(span1) - connectedSession.render(Seq(p1)) - connectedSession.renderChanges(Seq(p1.withChildren(span1.withText("span-text-changed")))) - span1.current.text should be("span-text-changed") diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 5c3b3799..987522a4 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -112,3 +112,15 @@ class ControllerTest extends AnyFunSuiteLike: .toList should be(List(0, 1)) Thread.sleep(15) rendered should be(Seq(button.withText("changed"))) + + test("current value for OnChange"): + val model = Model(0) + newController( + model, + Iterator(inputChange), + Seq( + input.onChange(using model): event => + import event.* + handled.withModel(if input.current.value == "new-value" then 100 else -1).terminate + ) + ).eventsIterator.toList should be(List(0, 100)) From c5f4022948913ba2cd7e53e790d4721f342c40db Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 23 Feb 2024 13:36:18 +0000 Subject: [PATCH 133/313] - --- .../scala/org/terminal21/client/ControllerTest.scala | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 987522a4..13c17ce4 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -124,3 +124,15 @@ class ControllerTest extends AnyFunSuiteLike: handled.withModel(if input.current.value == "new-value" then 100 else -1).terminate ) ).eventsIterator.toList should be(List(0, 100)) + + test("current value for OnChange/boolean"): + val model = Model(0) + newController( + model, + Iterator(checkBoxChange), + Seq( + checkbox.onChange(using model): event => + import event.* + handled.withModel(if checkbox.current.checked then 100 else -1).terminate + ) + ).eventsIterator.toList should be(List(0, 100)) From 1e0f0b9ad77ec89859535586771bdd045a543b7d Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 23 Feb 2024 14:01:04 +0000 Subject: [PATCH 134/313] - --- .../org/terminal21/client/Controller.scala | 100 ++++++++++-------- .../terminal21/client/ControllerTest.scala | 18 ++++ 2 files changed, 72 insertions(+), 46 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 62db7019..f466d5b6 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -42,58 +42,66 @@ class Controller[M]( (e.key, e.dataStore(initialModel.ChangeBooleanKey)) .toMap + private def componentsByKeyMap: Map[String, UiElement] = + components.flatMap(_.flat).map(c => (c.key, c)).toMap.withDefault(key => throw new IllegalArgumentException(s"Component with key=$key is not available")) + + private def updateComponentsByKeyFromEvent(handled: HandledEvent[M], event: CommandEvent): HandledEvent[M] = + handled.componentsByKey(event.key) match + case e: UiElement with HasEventHandler[_] => + event match + case OnChange(key, value) => + handled.copy(componentsByKey = handled.componentsByKey + (key -> e.defaultEventHandler(value))) + case _ => handled + case _ => handled + + private def invokeEventHandlers(handled: HandledEvent[M], componentsByKey: Map[String, UiElement], event: CommandEvent): HandledEvent[M] = + eventHandlers.foldLeft(handled.copy(renderChanges = Nil, timedRenderChanges = Nil)): (h, f) => + event match + case OnClick(key) => + f(ControllerClickEvent(componentsByKey(key), h)) + case OnChange(key, value) => + val receivedBy = componentsByKey(key) + val e = receivedBy match + case _: OnChangeEventHandler.CanHandleOnChangeEvent[_] => ControllerChangeEvent(receivedBy, h, value) + case _: OnChangeBooleanEventHandler.CanHandleOnChangeEvent[_] => ControllerChangeBooleanEvent(receivedBy, h, value.toBoolean) + f(e) + case x => throw new IllegalStateException(s"Unexpected state $x") + + private def invokeComponentEventHandlers(h: HandledEvent[M], event: CommandEvent) = + lazy val clickHandlers = clickHandlersMap(h) + lazy val changeHandlers = changeHandlersMap(h) + lazy val changeBooleanHandlers = changeBooleanHandlersMap(h) + event match + case OnClick(key) if clickHandlers.contains(key) => + val handlers = clickHandlers(key) + val receivedBy = h.componentsByKey(key) + val handled = handlers.foldLeft(h): (handled, handler) => + handler(ControllerClickEvent(receivedBy, handled)) + handled + case OnChange(key, value) if changeHandlers.contains(key) => + val handlers = changeHandlers(key) + val receivedBy = h.componentsByKey(key) + val handled = handlers.foldLeft(h): (handled, handler) => + handler(ControllerChangeEvent(receivedBy, handled, value)) + handled + case OnChange(key, value) if changeBooleanHandlers.contains(key) => + val handlers = changeBooleanHandlers(key) + val receivedBy = h.componentsByKey(key) + val handled = handlers.foldLeft(h): (handled, handler) => + handler(ControllerChangeBooleanEvent(receivedBy, handled, value.toBoolean)) + handled + case _ => h + def handledEventsIterator: EventIterator[HandledEvent[M]] = - val componentsByKey = - components.flatMap(_.flat).map(c => (c.key, c)).toMap.withDefault(key => throw new IllegalArgumentException(s"Component with key=$key is not available")) + val componentsByKey = componentsByKeyMap new EventIterator( eventIteratorFactory .takeWhile(!_.isSessionClosed) .scanLeft(HandledEvent(initialModel.value, componentsByKey, Nil, Nil, false)): (oldHandled, event) => - val initHandled = oldHandled.componentsByKey(event.key) match - case e: UiElement with HasEventHandler[_] => - event match - case OnChange(key, value) => - oldHandled.copy(componentsByKey = oldHandled.componentsByKey + (key -> e.defaultEventHandler(value))) - case _ => oldHandled - case _ => oldHandled - - val h = eventHandlers.foldLeft(initHandled.copy(renderChanges = Nil, timedRenderChanges = Nil)): (h, f) => - event match - case OnClick(key) => - f(ControllerClickEvent(componentsByKey(key), h)) - case OnChange(key, value) => - val receivedBy = componentsByKey(key) - val e = receivedBy match - case _: OnChangeEventHandler.CanHandleOnChangeEvent[_] => ControllerChangeEvent(receivedBy, h, value) - case _: OnChangeBooleanEventHandler.CanHandleOnChangeEvent[_] => ControllerChangeBooleanEvent(receivedBy, h, value.toBoolean) - f(e) - case x => throw new IllegalStateException(s"Unexpected state $x") - - lazy val clickHandlers = clickHandlersMap(h) - lazy val changeHandlers = changeHandlersMap(h) - lazy val changeBooleanHandlers = changeBooleanHandlersMap(h) - - val handled = event match - case OnClick(key) if clickHandlers.contains(key) => - val handlers = clickHandlers(key) - val receivedBy = h.componentsByKey(key) - val handled = handlers.foldLeft(h): (handled, handler) => - handler(ControllerClickEvent(receivedBy, handled)) - handled - case OnChange(key, value) if changeHandlers.contains(key) => - val handlers = changeHandlers(key) - val receivedBy = h.componentsByKey(key) - val handled = handlers.foldLeft(h): (handled, handler) => - handler(ControllerChangeEvent(receivedBy, handled, value)) - handled - case OnChange(key, value) if changeBooleanHandlers.contains(key) => - val handlers = changeBooleanHandlers(key) - val receivedBy = h.componentsByKey(key) - val handled = handlers.foldLeft(h): (handled, handler) => - handler(ControllerChangeBooleanEvent(receivedBy, handled, value.toBoolean)) - handled - case _ => h + val initHandled = updateComponentsByKeyFromEvent(oldHandled, event) + val h = invokeEventHandlers(initHandled, componentsByKey, event) + val handled = invokeComponentEventHandlers(h, event) handled .tapEach: handled => renderChanges(handled.renderChanges) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 13c17ce4..d7f86a10 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -102,6 +102,24 @@ class ControllerTest extends AnyFunSuiteLike: rendered should be(Seq(button.withText("changed"))) + test("changes are rendered once"): + var rendered = Seq.empty[UiElement] + def renderer(s: Seq[UiElement]): Unit = rendered = s + + val model = Model(0) + val handled = newController( + model, + Iterator(buttonClick, checkBoxChange), + Seq( + button.onClick(using model): event => + event.handled.withRenderChanges(button.withText("changed")), + checkbox + ), + renderer + ).handledEventsIterator.toList + + println(handled.mkString("\n")) + test("timed changes are rendered"): @volatile var rendered = Seq.empty[UiElement] def renderer(s: Seq[UiElement]): Unit = rendered = s From 838a83bb96a57f6fc7404bcdfb6ad87ed6e11005 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 23 Feb 2024 14:08:43 +0000 Subject: [PATCH 135/313] - --- .../src/test/scala/org/terminal21/client/ControllerTest.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index d7f86a10..ad04eb63 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -118,7 +118,8 @@ class ControllerTest extends AnyFunSuiteLike: renderer ).handledEventsIterator.toList - println(handled.mkString("\n")) + handled(1).renderChanges should be(List(button.withText("changed"))) + handled(2).renderChanges should be(Nil) test("timed changes are rendered"): @volatile var rendered = Seq.empty[UiElement] From 4afedb786851e540d7f7dbbc615e95435a4e4cc1 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 23 Feb 2024 14:58:41 +0000 Subject: [PATCH 136/313] - --- .../org/terminal21/client/Controller.scala | 34 +++++++++++-------- .../terminal21/client/ControllerTest.scala | 24 +++++++++---- 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index f466d5b6..0ee2de10 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -10,7 +10,7 @@ import org.terminal21.model.{CommandEvent, OnChange, OnClick} class Controller[M]( eventIteratorFactory: => Iterator[CommandEvent], renderChanges: Seq[UiElement] => Unit, - components: Seq[UiElement], + initialComponents: Seq[UiElement], initialModel: Model[M], eventHandlers: Seq[ControllerEvent[M] => HandledEvent[M]] ): @@ -18,7 +18,7 @@ class Controller[M]( new Controller( eventIteratorFactory, renderChanges, - components, + initialComponents, initialModel, eventHandlers :+ handler ) @@ -42,8 +42,12 @@ class Controller[M]( (e.key, e.dataStore(initialModel.ChangeBooleanKey)) .toMap - private def componentsByKeyMap: Map[String, UiElement] = - components.flatMap(_.flat).map(c => (c.key, c)).toMap.withDefault(key => throw new IllegalArgumentException(s"Component with key=$key is not available")) + private def initialComponentsByKeyMap: Map[String, UiElement] = + initialComponents + .flatMap(_.flat) + .map(c => (c.key, c)) + .toMap + .withDefault(key => throw new IllegalArgumentException(s"Component with key=$key is not available")) private def updateComponentsByKeyFromEvent(handled: HandledEvent[M], event: CommandEvent): HandledEvent[M] = handled.componentsByKey(event.key) match @@ -54,13 +58,13 @@ class Controller[M]( case _ => handled case _ => handled - private def invokeEventHandlers(handled: HandledEvent[M], componentsByKey: Map[String, UiElement], event: CommandEvent): HandledEvent[M] = + private def invokeEventHandlers(handled: HandledEvent[M], event: CommandEvent): HandledEvent[M] = eventHandlers.foldLeft(handled.copy(renderChanges = Nil, timedRenderChanges = Nil)): (h, f) => event match case OnClick(key) => - f(ControllerClickEvent(componentsByKey(key), h)) + f(ControllerClickEvent(h.componentsByKey(key), h)) case OnChange(key, value) => - val receivedBy = componentsByKey(key) + val receivedBy = h.componentsByKey(key) val e = receivedBy match case _: OnChangeEventHandler.CanHandleOnChangeEvent[_] => ControllerChangeEvent(receivedBy, h, value) case _: OnChangeBooleanEventHandler.CanHandleOnChangeEvent[_] => ControllerChangeBooleanEvent(receivedBy, h, value.toBoolean) @@ -92,17 +96,19 @@ class Controller[M]( handled case _ => h - def handledEventsIterator: EventIterator[HandledEvent[M]] = - val componentsByKey = componentsByKeyMap + private def includeRendered(handled: HandledEvent[M]): HandledEvent[M] = + val newComponentsByKey = handled.renderChanges.flatMap(_.flat).map(e => (e.key, e)).toMap + handled.copy(componentsByKey = handled.componentsByKey ++ newComponentsByKey) + def handledEventsIterator: EventIterator[HandledEvent[M]] = new EventIterator( eventIteratorFactory .takeWhile(!_.isSessionClosed) - .scanLeft(HandledEvent(initialModel.value, componentsByKey, Nil, Nil, false)): (oldHandled, event) => - val initHandled = updateComponentsByKeyFromEvent(oldHandled, event) - val h = invokeEventHandlers(initHandled, componentsByKey, event) - val handled = invokeComponentEventHandlers(h, event) - handled + .scanLeft(HandledEvent(initialModel.value, initialComponentsByKeyMap, Nil, Nil, false)): (oldHandled, event) => + val handled1 = includeRendered(updateComponentsByKeyFromEvent(oldHandled, event)) + val handled2 = includeRendered(invokeEventHandlers(handled1, event)) + val handled3 = includeRendered(invokeComponentEventHandlers(handled2, event)) + handled3 .tapEach: handled => renderChanges(handled.renderChanges) for trc <- handled.timedRenderChanges do diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index ad04eb63..09faa6f8 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -3,7 +3,7 @@ package org.terminal21.client import org.scalatest.funsuite.AnyFunSuiteLike import org.scalatest.matchers.should.Matchers.* import org.terminal21.client.components.UiElement -import org.terminal21.client.components.chakra.{Button, Checkbox} +import org.terminal21.client.components.chakra.{Box, Button, Checkbox} import org.terminal21.client.components.std.Input import org.terminal21.model.{CommandEvent, OnChange, OnClick} @@ -34,18 +34,18 @@ class ControllerTest extends AnyFunSuiteLike: test("onEvent is called for change"): val model = Model(0) newController(model, Iterator(inputChange), Seq(input)) - .onEvent: - case event @ ControllerChangeEvent(`input`, handled, "new-value") => - if event.model > 1 then handled.terminate else handled.withModel(event.model + 1) + .onEvent: event => + import event.* + if event.model > 1 then handled.terminate else handled.withModel(event.model + 1) .eventsIterator .toList should be(List(0, 1)) test("onEvent is called for change/boolean"): val model = Model(0) newController(model, Iterator(checkBoxChange), Seq(checkbox)) - .onEvent: - case event @ ControllerChangeBooleanEvent(`checkbox`, handled, true) => - if event.model > 1 then handled.terminate else handled.withModel(event.model + 1) + .onEvent: event => + import event.* + if event.model > 1 then handled.terminate else handled.withModel(event.model + 1) .eventsIterator .toList should be(List(0, 1)) @@ -155,3 +155,13 @@ class ControllerTest extends AnyFunSuiteLike: handled.withModel(if checkbox.current.checked then 100 else -1).terminate ) ).eventsIterator.toList should be(List(0, 100)) + + test("newly rendered elements are visible"): + val model = Model(0) + lazy val box: Box = Box().withChildren( + button.onClick(using model): event => + event.handled.withRenderChanges(box.withChildren(button, checkbox)) + ) + + val handledEvents = newController(model, Iterator(buttonClick), Seq(box)).handledEventsIterator.toList + handledEvents(1).componentsByKey(checkbox.key) should be(checkbox) From f97d49147bbadcbf5f10fe47b86afca4ed80ad42 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 23 Feb 2024 15:05:02 +0000 Subject: [PATCH 137/313] - --- .../org/terminal21/client/ControllerTest.scala | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 09faa6f8..956a530f 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -165,3 +165,20 @@ class ControllerTest extends AnyFunSuiteLike: val handledEvents = newController(model, Iterator(buttonClick), Seq(box)).handledEventsIterator.toList handledEvents(1).componentsByKey(checkbox.key) should be(checkbox) + + test("newly rendered elements event handlers are invoked"): + val model = Model(0) + lazy val b: Button = button.onClick(using model): event => + event.handled + .withModel(1) + .withRenderChanges( + box.withChildren( + b, + checkbox.onChange(using model): event => + event.handled.withModel(2) + ) + ) + + lazy val box: Box = Box().withChildren(b) + + newController(model, Iterator(buttonClick, checkBoxChange), Seq(box)).eventsIterator.toList should be(List(0, 1, 2)) From a785cb31115f691464283f498080e8e6779260d9 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 23 Feb 2024 15:11:41 +0000 Subject: [PATCH 138/313] - --- .../src/test/scala/org/terminal21/client/ControllerTest.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 956a530f..27080115 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -126,7 +126,7 @@ class ControllerTest extends AnyFunSuiteLike: def renderer(s: Seq[UiElement]): Unit = rendered = s newController(Model(0), Iterator(buttonClick), Seq(button), renderer) .onEvent: event => - event.handled.withModel(event.model + 1).withTimedRenderChanges(TimedRenderChanges(10, button.withText("changed"))).terminate + event.handled.withModel(1).withTimedRenderChanges(TimedRenderChanges(10, button.withText("changed"))).terminate .eventsIterator .toList should be(List(0, 1)) Thread.sleep(15) From 93a58ecc7fd9028528acaa2a413c408861f1d5a8 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 23 Feb 2024 15:19:52 +0000 Subject: [PATCH 139/313] - --- .../main/scala/org/terminal21/client/Controller.scala | 8 +++++--- .../scala/org/terminal21/client/ControllerTest.scala | 11 +++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 0ee2de10..3191f63c 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -97,7 +97,8 @@ class Controller[M]( case _ => h private def includeRendered(handled: HandledEvent[M]): HandledEvent[M] = - val newComponentsByKey = handled.renderChanges.flatMap(_.flat).map(e => (e.key, e)).toMap + val newComponentsByKey = + (handled.renderChanges.flatMap(_.flat) ++ handled.timedRenderChanges.flatMap(_.renderChanges).flatMap(_.flat)).map(e => (e.key, e)).toMap handled.copy(componentsByKey = handled.componentsByKey ++ newComponentsByKey) def handledEventsIterator: EventIterator[HandledEvent[M]] = @@ -125,9 +126,9 @@ object Controller: new Controller(session.eventIterator, session.renderChanges, components, initialModel, Nil) trait ControllerEvent[M]: - def model: M = handled.model + def model: M = handled.model def handled: HandledEvent[M] - extension [A <: UiElement](e: UiElement with HasEventHandler[A]) def current: A = handled.componentsByKey(e.key).asInstanceOf[A] + extension [A <: UiElement](e: A) def current: A = handled.current(e) case class ControllerClickEvent[M](clicked: UiElement, handled: HandledEvent[M]) extends ControllerEvent[M] case class ControllerChangeEvent[M](changed: UiElement, handled: HandledEvent[M], newValue: String) extends ControllerEvent[M] @@ -147,6 +148,7 @@ case class HandledEvent[M]( def withTimedRenderChanges(changed: TimedRenderChanges*): HandledEvent[M] = copy(timedRenderChanges = changed) def addTimedRenderChange(waitInMs: Long, renderChanges: UiElement): HandledEvent[M] = copy(timedRenderChanges = timedRenderChanges :+ TimedRenderChanges(waitInMs, renderChanges)) + def current[A <: UiElement](e: A): A = componentsByKey(e.key).asInstanceOf[A] type OnClickEventHandlerFunction[M] = ControllerClickEvent[M] => HandledEvent[M] type OnChangeEventHandlerFunction[M] = ControllerChangeEvent[M] => HandledEvent[M] diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 27080115..4618eaa9 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -132,6 +132,17 @@ class ControllerTest extends AnyFunSuiteLike: Thread.sleep(15) rendered should be(Seq(button.withText("changed"))) + test("timed changes are visible"): + val model = Model(0) + newController( + model, + Iterator(buttonClick), + Seq( + button.onClick(using model): event => + event.handled.withTimedRenderChanges(TimedRenderChanges(10, button.withText("changed"))).terminate + ) + ).handledEventsIterator.toList(1).current(button) should be(button.withText("changed")) + test("current value for OnChange"): val model = Model(0) newController( From 456dd78ad9e8b12bd74e1574dee7b337c9925478 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 23 Feb 2024 15:43:38 +0000 Subject: [PATCH 140/313] - --- .../scala/org/terminal21/client/Controller.scala | 4 ++-- .../org/terminal21/client/ControllerTest.scala | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 3191f63c..7fbe585a 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -49,7 +49,7 @@ class Controller[M]( .toMap .withDefault(key => throw new IllegalArgumentException(s"Component with key=$key is not available")) - private def updateComponentsByKeyFromEvent(handled: HandledEvent[M], event: CommandEvent): HandledEvent[M] = + private def updateComponentsFromEvent(handled: HandledEvent[M], event: CommandEvent): HandledEvent[M] = handled.componentsByKey(event.key) match case e: UiElement with HasEventHandler[_] => event match @@ -106,7 +106,7 @@ class Controller[M]( eventIteratorFactory .takeWhile(!_.isSessionClosed) .scanLeft(HandledEvent(initialModel.value, initialComponentsByKeyMap, Nil, Nil, false)): (oldHandled, event) => - val handled1 = includeRendered(updateComponentsByKeyFromEvent(oldHandled, event)) + val handled1 = includeRendered(updateComponentsFromEvent(oldHandled, event)) val handled2 = includeRendered(invokeEventHandlers(handled1, event)) val handled3 = includeRendered(invokeComponentEventHandlers(handled2, event)) handled3 diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 4618eaa9..8e9ed556 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -143,6 +143,19 @@ class ControllerTest extends AnyFunSuiteLike: ) ).handledEventsIterator.toList(1).current(button) should be(button.withText("changed")) + test("timed changes event handlers are called"): + val model = Model(0) + val c = checkbox.onChange(using model): event => + event.handled.withModel(2) + newController( + model, + Iterator(buttonClick, checkBoxChange), + Seq( + button.onClick(using model): event => + event.handled.withTimedRenderChanges(TimedRenderChanges(10, c)) + ) + ).eventsIterator.toList should be(List(0, 0, 2)) + test("current value for OnChange"): val model = Model(0) newController( From 18ee26f2361a1a8719308ba8b747687671dddf5d Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 23 Feb 2024 16:29:49 +0000 Subject: [PATCH 141/313] - --- .../main/scala/tests/ChakraComponents.scala | 28 +++---- .../src/main/scala/tests/LoginForm.scala | 58 +++++++------- .../main/scala/tests/MathJaxComponents.scala | 8 +- .../src/main/scala/tests/NivoComponents.scala | 5 +- .../scala/tests/StateSessionStateBug.scala | 16 ++-- .../src/main/scala/tests/StdComponents.scala | 18 +++-- .../src/main/scala/tests/chakra/Buttons.scala | 18 ++--- .../main/scala/tests/chakra/Editables.scala | 14 ++-- .../src/main/scala/tests/chakra/Forms.scala | 80 +++++++++++-------- .../main/scala/tests/chakra/Navigation.scala | 27 ++++--- .../src/main/scala/tests/chakra/Overlay.scala | 27 ++++--- .../src/test/scala/tests/LoggedInTest.scala | 2 - .../src/test/scala/tests/LoginFormTest.scala | 2 - .../org/terminal21/client/Controller.scala | 7 ++ .../components/chakra/ChakraElement.scala | 18 ++--- 15 files changed, 184 insertions(+), 144 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala index cd959357..8cb6630b 100644 --- a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala @@ -1,8 +1,8 @@ package tests import org.terminal21.client.* +import org.terminal21.client.components.UiElement import org.terminal21.client.components.chakra.* -import org.terminal21.client.components.render import org.terminal21.client.components.std.Paragraph import tests.chakra.* @@ -12,30 +12,26 @@ import java.util.concurrent.atomic.AtomicBoolean @main def chakraComponents(): Unit = val keepRunning = new AtomicBoolean(true) - while keepRunning.get() do + def loop(): Unit = println("Starting new session") Sessions .withNewSession("chakra-components", "Chakra Components") .connect: session => keepRunning.set(false) - given ConnectedSession = session - given controller: Controller[Boolean] = Controller(false) + given ConnectedSession = session + given model: Model[Boolean] = Model(false) // react tests reset the session to clear state val krButton = Button(text = "Reset state").onClick: event => keepRunning.set(true) event.handled.terminate - (Overlay.components ++ Forms.components ++ Editables.components ++ Stacks.components ++ Grids.components ++ Buttons.components( - latch - ) ++ Etc.components ++ MediaAndIcons.components ++ DataDisplay.components ++ Typography.components ++ Feedback.components ++ Disclosure.components ++ Navigation.components ++ Seq( - krButton - )) - .render() + val components: Seq[UiElement] = + Overlay.components ++ Forms.components ++ Editables.components ++ Stacks.components ++ Grids.components ++ Buttons.components ++ Etc.components ++ MediaAndIcons.components ++ DataDisplay.components ++ Typography.components ++ Feedback.components ++ Disclosure.components ++ Navigation.components ++ Seq( + krButton + ) + Controller(components).eventsIterator.lastOption match + case Some(true) => loop() + case _ => - println("Waiting for button to be pressed for 1 hour") - session.waitTillUserClosesSessionOr(latch.getCount == 0) - if !session.isClosed then - session.clear() - Paragraph(text = "Terminated").render() - Thread.sleep(1000) + loop() diff --git a/end-to-end-tests/src/main/scala/tests/LoginForm.scala b/end-to-end-tests/src/main/scala/tests/LoginForm.scala index 4dc9dde6..b9e8cb41 100644 --- a/end-to-end-tests/src/main/scala/tests/LoginForm.scala +++ b/end-to-end-tests/src/main/scala/tests/LoginForm.scala @@ -3,7 +3,7 @@ package tests import org.terminal21.client.components.* import org.terminal21.client.components.chakra.* import org.terminal21.client.components.std.{NewLine, Paragraph} -import org.terminal21.client.{ConnectedSession, Controller, Sessions} +import org.terminal21.client.* @main def loginFormApp(): Unit = Sessions @@ -23,18 +23,28 @@ case class Login(email: String, pwd: String): /** The login form. Displays an email and password input and a submit button. When run() it will fill in the Login(email,pwd) model. */ class LoginForm(using session: ConnectedSession): - private val initialModel = Login("my@email.com", "mysecret") - val okIcon = CheckCircleIcon(color = Some("green")) - val notOkIcon = WarningTwoIcon(color = Some("red")) - val emailRightAddon = InputRightAddon().withChildren(okIcon) - val emailInput = Input(`type` = "email", defaultValue = initialModel.email) - val submitButton = Button(text = "Submit") - val passwordInput = Input(`type` = "password", defaultValue = initialModel.pwd) + private given initialModel: Model[Login] = Model(Login("my@email.com", "mysecret")) + val okIcon = CheckCircleIcon(color = Some("green")) + val notOkIcon = WarningTwoIcon(color = Some("red")) + val emailRightAddon = InputRightAddon().withChildren(okIcon) + val emailInput = Input(`type` = "email", defaultValue = initialModel.value.email) + .onChange: changeEvent => + changeEvent.handled.withRenderChanges(validate(changeEvent.model)) + + val submitButton = Button(text = "Submit") + .onClick: clickEvent => + import clickEvent.* + // if the email is invalid, we will not terminate. We also will render an error that will be visible for 2 seconds + val isValidEmail = clickEvent.model.isValidEmail + val messageBox = + if isValidEmail then errorsBox.current else errorsBox.current.addChildren(errorMsgInvalidEmail) + clickEvent.handled.withShouldTerminate(isValidEmail).withRenderChanges(messageBox).addTimedRenderChange(2000, errorsBox) + + val passwordInput = Input(`type` = "password", defaultValue = initialModel.value.pwd) val errorsBox = Box() val errorMsgInvalidEmail = Paragraph(text = "Invalid Email", style = Map("color" -> "red")) def run(): Option[Login] = - components.render() controller.eventsIterator.lastOptionOrNoneIfSessionClosed def components: Seq[UiElement] = @@ -58,31 +68,29 @@ class LoginForm(using session: ConnectedSession): errorsBox ) - def controller: Controller[Login] = Controller(initialModel) + def controller: Controller[Login] = Controller(components) .onEvent: event => + import event.* val newModel = event.model.copy(email = emailInput.current.value, pwd = passwordInput.current.value) event.handled.withModel(newModel) - .onClick(submitButton): clickEvent => - // if the email is invalid, we will not terminate. We also will render an error that will be visible for 2 seconds - val isValidEmail = clickEvent.model.isValidEmail - val messageBox = - if isValidEmail then errorsBox.current else errorsBox.current.addChildren(errorMsgInvalidEmail) - clickEvent.handled.withShouldTerminate(isValidEmail).withRenderChanges(messageBox).addTimedRenderChange(2000, errorsBox) - .onChange(emailInput): changeEvent => - changeEvent.handled.withRenderChanges(validate(changeEvent.model)) private def validate(login: Login): InputRightAddon = if login.isValidEmail then emailRightAddon.withChildren(okIcon) else emailRightAddon.withChildren(notOkIcon) class LoggedIn(login: Login)(using session: ConnectedSession): - val yesButton = Button(text = "Yes") - val noButton = Button(text = "No") + private given Model[Boolean] = Model(false) + val yesButton = Button(text = "Yes") + .onClick: e => + e.handled.withModel(true).terminate + + val noButton = Button(text = "No") + .onClick: e => + e.handled.withModel(false).terminate + val emailDetails = Text(text = s"email : ${login.email}") val passwordDetails = Text(text = s"password : ${login.pwd}") def run(): Option[Boolean] = - session.clear() // when transitioning to a new UI page, we need to clear previous event handlers, iterators etc. - components.render() // this will clear the UI and render the components for this form controller.eventsIterator.lastOption def components = @@ -100,8 +108,4 @@ class LoggedIn(login: Login)(using session: ConnectedSession): /** @return * A controller with a boolean value, true if user clicked "Yes", false for "No" */ - def controller = Controller(false) - .onClick(yesButton): e => - e.handled.withModel(true).terminate - .onClick(noButton): e => - e.handled.withModel(false).terminate + def controller = Controller(components) diff --git a/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala b/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala index 5c2465ca..1c0c3b35 100644 --- a/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala @@ -11,7 +11,9 @@ import org.terminal21.client.components.mathjax.* .andLibraries(MathJaxLib) .connect: session => given ConnectedSession = session - Seq( + import Model.unitModel + + val components = Seq( HStack().withChildren( Text(text = "Lets write some math expressions that will wow everybody!"), MathJax(expression = """\[\sum_{n = 200}^{1000}\left(\frac{20\sqrt{n}}{n}\right)\]""") @@ -21,5 +23,5 @@ import org.terminal21.client.components.mathjax.* expression = """Does it align correctly? \(ax^2 + bx + c = 0\) It does provided CHTML renderer is used.""", style = Map("backgroundColor" -> "gray") ) - ).render() - session.leaveSessionOpenAfterExiting() + ) + Controller(components).eventsIterator.lastOption diff --git a/end-to-end-tests/src/main/scala/tests/NivoComponents.scala b/end-to-end-tests/src/main/scala/tests/NivoComponents.scala index c861cc3c..ecb18dc0 100644 --- a/end-to-end-tests/src/main/scala/tests/NivoComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/NivoComponents.scala @@ -11,5 +11,6 @@ import tests.nivo.{ResponsiveBarChart, ResponsiveLineChart} .andLibraries(NivoLib) .connect: session => given ConnectedSession = session - (ResponsiveBarChart() ++ ResponsiveLineChart()).render() - session.waitTillUserClosesSession() + import Model.unitModel + val components = ResponsiveBarChart() ++ ResponsiveLineChart() + Controller(components).eventsIterator.lastOption diff --git a/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala b/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala index f1d9cf64..b248b471 100644 --- a/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala +++ b/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala @@ -3,7 +3,7 @@ package tests import org.terminal21.client.components.* import org.terminal21.client.components.chakra.* import org.terminal21.client.components.std.Paragraph -import org.terminal21.client.{ConnectedSession, Sessions} +import org.terminal21.client.* import java.util.Date @@ -12,10 +12,10 @@ import java.util.Date .withNewSession("state-session", "Stale Session") .connect: session => given ConnectedSession = session + import Model.unitModel - var exitFlag = false - val date = new Date() - Seq( + val date = new Date() + val components = Seq( Paragraph(text = s"Now: $date"), QuickTable() .withHeaders("Title", "Value") @@ -39,7 +39,7 @@ import java.util.Date ) ) ), - Button(text = "Close").onClick: () => - exitFlag = true - ).render() - session.waitTillUserClosesSessionOr(exitFlag) + Button(text = "Close").onClick: event => + event.handled.terminate + ) + Controller(components).eventsIterator.lastOption diff --git a/end-to-end-tests/src/main/scala/tests/StdComponents.scala b/end-to-end-tests/src/main/scala/tests/StdComponents.scala index 17f576a3..c3be8210 100644 --- a/end-to-end-tests/src/main/scala/tests/StdComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/StdComponents.scala @@ -9,13 +9,15 @@ import org.terminal21.client.components.std.* .withNewSession("std-components", "Std Components") .connect: session => given ConnectedSession = session + import Model.unitModel val output = Paragraph(text = "This will reflect what you type in the input") val cookieValue = Paragraph(text = "This will display the value of the cookie") - val input = Input(defaultValue = "Please enter your name").onChange: newValue => - output.withText(newValue).renderChanges() + val input = Input(defaultValue = "Please enter your name").onChange: event => + import event.* + handled.withRenderChanges(output.withText(newValue)) - Seq( + val components = Seq( Header1(text = "header1 test"), Header2(text = "header2 test"), Header3(text = "header3 test"), @@ -34,9 +36,11 @@ import org.terminal21.client.components.std.* ), output, Cookie(name = "std-components-test-cookie", value = "test-cookie-value"), - CookieReader(name = "std-components-test-cookie").onChange: newValue => - cookieValue.withText(s"Cookie value $newValue").renderChanges(), + CookieReader(name = "std-components-test-cookie").onChange: event => + import event.* + handled.withRenderChanges(cookieValue.withText(s"Cookie value $newValue")) + , cookieValue - ).render() + ) - session.waitTillUserClosesSession() + Controller(components).eventsIterator.lastOption diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Buttons.scala b/end-to-end-tests/src/main/scala/tests/chakra/Buttons.scala index 7e3674e1..beb70126 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Buttons.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Buttons.scala @@ -1,6 +1,6 @@ package tests.chakra -import org.terminal21.client.ConnectedSession +import org.terminal21.client.{ConnectedSession, Model} import org.terminal21.client.components.* import org.terminal21.client.components.chakra.* import tests.chakra.Common.* @@ -8,16 +8,16 @@ import tests.chakra.Common.* import java.util.concurrent.CountDownLatch object Buttons: - def components(latch: CountDownLatch)(using session: ConnectedSession): Seq[UiElement] = + def components(using Model[Boolean]): Seq[UiElement] = val box1 = commonBox(text = "Buttons") val exitButton = Button(text = "Click to exit program", colorScheme = Some("red")) Seq( box1, - exitButton.onClick: () => - Seq( - box1.withText("Exit Clicked!"), - exitButton.withText("Stopping...").withColorScheme(Some("green")) - ).renderChanges() - Thread.sleep(1000) - latch.countDown() + exitButton.onClick: event => + event.handled + .withRenderChanges( + box1.withText("Exit Clicked!"), + exitButton.withText("Stopping...").withColorScheme(Some("green")) + ) + .terminate ) diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala b/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala index 9cb06032..c23cf651 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala @@ -1,12 +1,12 @@ package tests.chakra -import org.terminal21.client.ConnectedSession +import org.terminal21.client.{ConnectedSession, Model} import org.terminal21.client.components.UiElement import org.terminal21.client.components.chakra.* import tests.chakra.Common.* object Editables: - def components(using session: ConnectedSession): Seq[UiElement] = + def components(using ConnectedSession, Model[Boolean]): Seq[UiElement] = val status = Box(text = "This will reflect any changes in the form.") val editable1I = Editable(defaultValue = "Please type here").withChildren( @@ -14,15 +14,17 @@ object Editables: EditableInput() ) - val editable1 = editable1I.onChange: newValue => - status.withText(s"editable1 newValue = $newValue, verify editable1.value = ${editable1I.current.value}").renderChanges() + val editable1 = editable1I.onChange: event => + import event.* + handled.withRenderChanges(status.withText(s"editable1 newValue = $newValue, verify editable1.value = ${editable1I.current.value}")) val editable2I = Editable(defaultValue = "For longer maybe-editable texts\nUse an EditableTextarea\nIt uses a textarea control.").withChildren( EditablePreview(), EditableTextarea() ) - val editable2 = editable2I.onChange: newValue => - status.withText(s"editable2 newValue = $newValue, verify editable2.value = ${editable2I.current.value}").renderChanges() + val editable2 = editable2I.onChange: event => + import event.* + handled.withRenderChanges(status.withText(s"editable2 newValue = $newValue, verify editable2.value = ${editable2I.current.value}")) Seq( commonBox(text = "Editables"), diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala b/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala index d968a394..6fba0202 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala @@ -1,12 +1,12 @@ package tests.chakra -import org.terminal21.client.ConnectedSession +import org.terminal21.client.* import org.terminal21.client.components.* import org.terminal21.client.components.chakra.* import tests.chakra.Common.* object Forms: - def components(using session: ConnectedSession): Seq[UiElement] = + def components(using Model[Boolean]): Seq[UiElement] = val status = Box(text = "This will reflect any changes in the form.") val okIcon = CheckCircleIcon(color = Some("green")) val notOkIcon = WarningTwoIcon(color = Some("red")) @@ -14,23 +14,26 @@ object Forms: val emailRightAddOn = InputRightAddon().withChildren(okIcon) val emailI = Input(`type` = "email", defaultValue = "the-test-email@email.com") - val email = emailI.onChange: newValue => - Seq( + val email = emailI.onChange: event => + import event.* + handled.withRenderChanges( status.withText(s"email input new value = $newValue, verify email.value = ${emailI.current.value}"), if newValue.contains("@") then emailRightAddOn.withChildren(okIcon) else emailRightAddOn.withChildren(notOkIcon) - ).renderChanges() + ) val descriptionI = Textarea(placeholder = "Please enter a few things about you", defaultValue = "desc") - val description = descriptionI.onChange: newValue => - status.withText(s"description input new value = $newValue, verify description.value = ${descriptionI.current.value}").renderChanges() + val description = descriptionI.onChange: event => + import event.* + handled.withRenderChanges(status.withText(s"description input new value = $newValue, verify description.value = ${descriptionI.current.value}")) val select1I = Select(placeholder = "Please choose").withChildren( Option_(text = "Male", value = "male"), Option_(text = "Female", value = "female") ) - val select1 = select1I.onChange: newValue => - status.withText(s"select1 input new value = $newValue, verify select1.value = ${select1I.current.value}").renderChanges() + val select1 = select1I.onChange: event => + import event.* + handled.withRenderChanges(status.withText(s"select1 input new value = $newValue, verify select1.value = ${select1I.current.value}")) val select2 = Select(defaultValue = "1", bg = Some("tomato"), color = Some("black"), borderColor = Some("yellow")).withChildren( Option_(text = "First", value = "1"), @@ -39,33 +42,39 @@ object Forms: val password = Input(`type` = "password", defaultValue = "mysecret") val dobI = Input(`type` = "datetime-local") - val dob = dobI.onChange: newValue => - status.withText(s"dob = $newValue , verify dob.value = ${dobI.current.value}").renderChanges() + val dob = dobI.onChange: event => + import event.* + handled.withRenderChanges(status.withText(s"dob = $newValue , verify dob.value = ${dobI.current.value}")) val colorI = Input(`type` = "color") - val color = colorI.onChange: newValue => - status.withText(s"color = $newValue , verify color.value = ${colorI.current.value}").renderChanges() + val color = colorI.onChange: event => + import event.* + handled.withRenderChanges(status.withText(s"color = $newValue , verify color.value = ${colorI.current.value}")) val checkbox2I = Checkbox(text = "Check 2", defaultChecked = true) - val checkbox2 = checkbox2I.onChange: newValue => - status.withText(s"checkbox2 checked is $newValue , verify checkbox2.checked = ${checkbox2I.current.checked}").renderChanges() + val checkbox2 = checkbox2I.onChange: event => + import event.* + handled.withRenderChanges(status.withText(s"checkbox2 checked is $newValue , verify checkbox2.checked = ${checkbox2I.current.checked}")) val checkbox1I = Checkbox(text = "Check 1") - val checkbox1 = checkbox1I.onChange: newValue => - Seq( + val checkbox1 = checkbox1I.onChange: event => + import event.* + handled.withRenderChanges( status.withText(s"checkbox1 checked is $newValue , verify checkbox1.checked = ${checkbox1I.current.checked}"), checkbox2.withIsDisabled(newValue) - ).renderChanges() + ) val switch1I = Switch(text = "Switch 1") val switch2 = Switch(text = "Switch 2", defaultChecked = true) - val switch1 = switch1I.onChange: newValue => - Seq( - status.withText(s"switch1 checked is $newValue , verify switch1.checked = ${switch1I.current.checked}"), - switch2.withIsDisabled(newValue) - ).renderChanges() + val switch1 = switch1I.onChange: event => + import event.* + handled + .withRenderChanges( + status.withText(s"switch1 checked is $newValue , verify switch1.checked = ${switch1I.current.checked}"), + switch2.withIsDisabled(newValue) + ) val radioGroupI = RadioGroup(defaultValue = "2").withChildren( HStack().withChildren( @@ -75,8 +84,9 @@ object Forms: ) ) - val radioGroup = radioGroupI.onChange: newValue => - status.withText(s"radioGroup newValue=$newValue , verify radioGroup.value=${radioGroupI.current.value}").renderChanges() + val radioGroup = radioGroupI.onChange: event => + import event.* + handled.withRenderChanges(status.withText(s"radioGroup newValue=$newValue , verify radioGroup.value=${radioGroupI.current.value}")) Seq( commonBox(text = "Forms"), @@ -133,15 +143,19 @@ object Forms: ), ButtonGroup(variant = Some("outline"), spacing = Some("24")).withChildren( Button(text = "Save", colorScheme = Some("red")) - .onClick: () => - status - .withText( - s"Saved clicked. Email = ${email.current.value}, password = ${password.current.value}, dob = ${dob.current.value}, check1 = ${checkbox1.current.checked}, check2 = ${checkbox2.current.checked}, radio = ${radioGroup.current.value}, switch1 = ${switch1.current.checked}, switch2 = ${switch2.current.checked}" - ) - .renderChanges(), + .onClick: event => + import event.* + handled.withRenderChanges( + status + .withText( + s"Saved clicked. Email = ${email.current.value}, password = ${password.current.value}, dob = ${dob.current.value}, check1 = ${checkbox1.current.checked}, check2 = ${checkbox2.current.checked}, radio = ${radioGroup.current.value}, switch1 = ${switch1.current.checked}, switch2 = ${switch2.current.checked}" + ) + ) + , Button(text = "Cancel") - .onClick: () => - status.withText("Cancel clicked").renderChanges() + .onClick: event => + import event.* + handled.withRenderChanges(status.withText("Cancel clicked")) ), radioGroup, status diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Navigation.scala b/end-to-end-tests/src/main/scala/tests/chakra/Navigation.scala index afa7df70..6ed38edc 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Navigation.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Navigation.scala @@ -1,16 +1,15 @@ package tests.chakra -import org.terminal21.client.ConnectedSession +import org.terminal21.client.* import org.terminal21.client.components.UiElement import org.terminal21.client.components.chakra.* import org.terminal21.client.components.std.Paragraph import tests.chakra.Common.commonBox object Navigation: - def components(using session: ConnectedSession): Seq[UiElement] = - val clickedBreadcrumb = Paragraph(text = "no-breadcrumb-clicked") - def breadcrumbClicked(t: String): Unit = - clickedBreadcrumb.withText(s"breadcrumb-click: $t").renderChanges() + def components(using Model[Boolean]): Seq[UiElement] = + val clickedBreadcrumb = Paragraph(text = "no-breadcrumb-clicked") + def breadcrumbClicked(t: String) = clickedBreadcrumb.withText(s"breadcrumb-click: $t") val clickedLink = Paragraph(text = "no-link-clicked") @@ -18,19 +17,27 @@ object Navigation: commonBox(text = "Breadcrumbs"), Breadcrumb().withChildren( BreadcrumbItem().withChildren( - BreadcrumbLink(text = "breadcrumb-home").onClick(() => breadcrumbClicked("breadcrumb-home")) + BreadcrumbLink(text = "breadcrumb-home").onClick: event => + import event.* + handled.withRenderChanges(breadcrumbClicked("breadcrumb-home")) ), BreadcrumbItem().withChildren( - BreadcrumbLink(text = "breadcrumb-link1").onClick(() => breadcrumbClicked("breadcrumb-link1")) + BreadcrumbLink(text = "breadcrumb-link1").onClick: event => + import event.* + handled.withRenderChanges(breadcrumbClicked("breadcrumb-link1")) ), BreadcrumbItem(isCurrentPage = Some(true)).withChildren( - BreadcrumbLink(text = "breadcrumb-link2").onClick(() => breadcrumbClicked("breadcrumb-link2")) + BreadcrumbLink(text = "breadcrumb-link2").onClick: event => + import event.* + handled.withRenderChanges(breadcrumbClicked("breadcrumb-link2")) ) ), clickedBreadcrumb, commonBox(text = "Link"), Link(text = "link-external-google", href = "https://www.google.com/", isExternal = Some(true)) - .onClick: () => - clickedLink.withText("link-clicked").renderChanges(), + .onClick: event => + import event.* + handled.withRenderChanges(clickedLink.withText("link-clicked")) + , clickedLink ) diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala b/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala index bed13a35..f4f51d77 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala @@ -1,12 +1,12 @@ package tests.chakra -import org.terminal21.client.ConnectedSession +import org.terminal21.client.* import org.terminal21.client.components.UiElement import org.terminal21.client.components.chakra.* import tests.chakra.Common.commonBox object Overlay: - def components(using session: ConnectedSession): Seq[UiElement] = + def components(using Model[Boolean]): Seq[UiElement] = val box1 = Box(text = "Clicks will be reported here.") Seq( commonBox(text = "Menus box0001"), @@ -17,15 +17,22 @@ object Overlay: ), MenuList().withChildren( MenuItem(text = "Download menu-download") - .onClick: () => - box1.withText("'Download' clicked").renderChanges(), - MenuItem(text = "Copy").onClick: () => - box1.withText("'Copy' clicked").renderChanges(), - MenuItem(text = "Paste").onClick: () => - box1.withText("'Paste' clicked").renderChanges(), + .onClick: event => + import event.* + handled.withRenderChanges(box1.withText("'Download' clicked")) + , + MenuItem(text = "Copy").onClick: event => + import event.* + handled.withRenderChanges(box1.withText("'Copy' clicked")) + , + MenuItem(text = "Paste").onClick: event => + import event.* + handled.withRenderChanges(box1.withText("'Paste' clicked")) + , MenuDivider(), - MenuItem(text = "Exit").onClick: () => - box1.withText("'Exit' clicked").renderChanges() + MenuItem(text = "Exit").onClick: event => + import event.* + handled.withRenderChanges(box1.withText("'Exit' clicked")) ) ), box1 diff --git a/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala b/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala index 4ed895ab..8b70aa42 100644 --- a/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala +++ b/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala @@ -23,14 +23,12 @@ class LoggedInTest extends AnyFunSuiteLike: test("yes clicked"): new App: - form.components.render() val eventsIt = form.controller.eventsIterator session.fireEvents(CommandEvent.onClick(form.yesButton), CommandEvent.sessionClosed) eventsIt.lastOption should be(Some(true)) test("no clicked"): new App: - form.components.render() val eventsIt = form.controller.eventsIterator session.fireEvents(CommandEvent.onClick(form.noButton), CommandEvent.sessionClosed) eventsIt.lastOption should be(Some(false)) diff --git a/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala b/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala index 66d1f9da..5ed7d3f4 100644 --- a/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala +++ b/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala @@ -27,7 +27,6 @@ class LoginFormTest extends AnyFunSuiteLike: test("user submits validated data"): new App: - form.components.render() val eventsIt = form.controller.eventsIterator // get the iterator before we fire the events, otherwise the iterator will be empty session.fireEvents( CommandEvent.onChange(form.emailInput, "an@email.com"), @@ -40,7 +39,6 @@ class LoginFormTest extends AnyFunSuiteLike: test("user submits invalid email"): new App: - form.components.render() val eventsIt = form.controller.handledEventsIterator // get the iterator that iterates Handled instances so that we can assert on renderChanges session.fireEvents( CommandEvent.onChange(form.emailInput, "invalid-email.com"), diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 7fbe585a..68cf7ce4 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -124,6 +124,8 @@ class Controller[M]( object Controller: def apply[M](initialModel: Model[M], components: Seq[UiElement])(using session: ConnectedSession): Controller[M] = new Controller(session.eventIterator, session.renderChanges, components, initialModel, Nil) + def apply[M](components: Seq[UiElement])(using initialModel: Model[M], session: ConnectedSession): Controller[M] = + new Controller(session.eventIterator, session.renderChanges, components, initialModel, Nil) trait ControllerEvent[M]: def model: M = handled.model @@ -162,3 +164,8 @@ case class Model[M](value: M): object ClickKey extends TypedMapKey[Seq[OnClickEventHandlerFunction[M]]] object ChangeKey extends TypedMapKey[Seq[OnChangeEventHandlerFunction[M]]] object ChangeBooleanKey extends TypedMapKey[Seq[OnChangeBooleanEventHandlerFunction[M]]] + +object Model: + given unitModel: Model[Unit] = Model(()) + given booleanFalseModel: Model[Boolean] = Model(false) + given booleanTrueModel: Model[Boolean] = Model(true) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala index dac20548..aa747010 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala @@ -89,15 +89,15 @@ case class Box( children: Seq[UiElement] = Nil ) extends ChakraElement[Box] with HasChildren[Box]: - override def withChildren(cn: UiElement*) = copy(children = cn) - override def withStyle(v: Map[String, Any]) = copy(style = v) - def withKey(v: String) = copy(key = v) - def withText(v: String) = copy(text = v) - def withBg(v: String) = copy(bg = v) - def withW(v: String) = copy(w = v) - def withP(v: Int) = copy(p = v) - def withColor(v: String) = copy(color = v) - def withAs(v: Option[String]) = copy(as = v) + override def withChildren(cn: UiElement*): Box = copy(children = cn) + override def withStyle(v: Map[String, Any]): Box = copy(style = v) + def withKey(v: String): Box = copy(key = v) + def withText(v: String): Box = copy(text = v) + def withBg(v: String): Box = copy(bg = v) + def withW(v: String): Box = copy(w = v) + def withP(v: Int): Box = copy(p = v) + def withColor(v: String): Box = copy(color = v) + def withAs(v: Option[String]): Box = copy(as = v) /** https://chakra-ui.com/docs/components/stack */ From 35b61df063191e28742f07d05dd064a0784a62d2 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 23 Feb 2024 16:51:19 +0000 Subject: [PATCH 142/313] - --- .../serverapp/bundled/AppManager.scala | 30 ++++++++------ .../serverapp/bundled/ServerStatusApp.scala | 39 ++++++++++--------- .../serverapp/bundled/SettingsApp.scala | 6 +-- .../bundled/AppManagerPageTest.scala | 4 +- .../serverapp/bundled/SettingsPageTest.scala | 8 ---- 5 files changed, 43 insertions(+), 44 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala index 1d31da9d..bc9e6b2d 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala @@ -1,7 +1,7 @@ package org.terminal21.serverapp.bundled import functions.fibers.FiberExecutor -import org.terminal21.client.{ConnectedSession, Controller} +import org.terminal21.client.* import org.terminal21.client.components.* import org.terminal21.client.components.chakra.* import org.terminal21.client.components.std.{Header1, Paragraph, Span} @@ -24,15 +24,24 @@ class AppManager(serverSideSessions: ServerSideSessions, fiberExecutor: FiberExe app.createSession(serverSideSessions, dependencies) class AppManagerPage(apps: Seq[ServerSideApp], startApp: ServerSideApp => Unit)(using session: ConnectedSession): + case class ManagerModel(startApp: Option[ServerSideApp] = None) + given Model[ManagerModel] = Model(ManagerModel()) + def run(): Unit = - components.render() eventsIterator.foreach(_ => ()) case class AppRow(app: ServerSideApp, link: Link, text: Text): def row: Seq[UiElement] = Seq(link, text) val appRows = apps.map: app => - AppRow(app, Link(text = app.name), Text(text = app.description)) + AppRow( + app, + Link(text = app.name).onClick: event => + import event.* + handled.withModel(model.copy(startApp = Some(app))) + , + Text(text = app.description) + ) def components = val appsTable = QuickTable( @@ -58,17 +67,16 @@ class AppManagerPage(apps: Seq[ServerSideApp], startApp: ServerSideApp => Unit)( ) ) - case class Model(startApp: Option[ServerSideApp] = None) - def controller: Controller[Model] = - appRows - .foldLeft(Controller(Model())): (c, appRow) => - c.onClick(appRow.link): event => - event.handled.withModel(Model(startApp = Some(appRow.app))) + def controller(components: Seq[UiElement]): Controller[ManagerModel] = + Controller(components) .onEvent: event => + import event.* // for every event, reset the model - event.handled.withModel(event.model.copy(startApp = None)) + handled.withModel(model.copy(startApp = None)) + + def controller: Controller[ManagerModel] = controller(components) - def eventsIterator: Iterator[Model] = + def eventsIterator: Iterator[ManagerModel] = controller.eventsIterator .tapEach: m => for app <- m.startApp do startApp(app) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 5f1cddb4..5ea2dd12 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -1,9 +1,9 @@ package org.terminal21.serverapp.bundled -import org.terminal21.client.{ConnectedSession, Controller} +import org.terminal21.client.* import org.terminal21.client.components.* import org.terminal21.client.components.chakra.* -import org.terminal21.model.Session +import org.terminal21.model.{Session, SessionOptions} import org.terminal21.server.Dependencies import org.terminal21.server.model.SessionState import org.terminal21.server.service.ServerSessionsService @@ -23,18 +23,16 @@ class ServerStatusPage( serverSideSessions: ServerSideSessions, sessionsService: ServerSessionsService )(using session: ConnectedSession): - def run(): Unit = - while !session.isClosed do - updateStatus() - Thread.sleep(1000) + import Model.unitModel + def run(): Unit = controller(Runtime.getRuntime, sessionsService.allSessions).eventsIterator.lastOption - private def toMb(v: Long) = s"${v / (1024 * 1024)} MB" - private val xs = Some("2xs") - private def updateStatus(): Unit = - given Controller[Unit] = Controller(()) - components(Runtime.getRuntime, sessionsService.allSessions).render() + private def toMb(v: Long) = s"${v / (1024 * 1024)} MB" + private val xs = Some("2xs") - def components(runtime: Runtime, sessions: Seq[Session])(using Controller[Unit]): Seq[UiElement] = + def controller(runtime: Runtime, sessions: Seq[Session]): Controller[Unit] = + Controller(components(runtime, sessions)) + + def components(runtime: Runtime, sessions: Seq[Session]): Seq[UiElement] = val jvmTable = QuickTable(caption = Some("JVM")) .withHeaders("Property", "Value", "Actions") .withRows( @@ -60,23 +58,26 @@ class ServerStatusPage( Seq(jvmTable, sessionsTable) - private def actionsFor(session: Session)(using ConnectedSession): UiElement = + private def actionsFor(session: Session): UiElement = if session.isOpen then Box().withChildren( Button(text = "Close", size = xs) .withLeftIcon(SmallCloseIcon()) - .onClick: () => + .onClick: event => + import event.* sessionsService.terminateAndRemove(session) - updateStatus() + handled , Text(text = " "), Button(text = "View State", size = xs) .withLeftIcon(ChatIcon()) - .onClick: () => + .onClick: event => serverSideSessions .withNewSession(session.id + "-server-state", s"Server State:${session.id}") + .andOptions(SessionOptions.LeaveOpenWhenTerminated) .connect: sSession => new ViewServerStatePage(using sSession).runFor(sessionsService.sessionStateOf(session)) + event.handled ) else NotAllowedIcon() @@ -103,12 +104,12 @@ class ViewServerStatePage(using session: ConnectedSession): ) ) - Seq( + val components = Seq( QuickTabs() .withTabs("Root Keys", "Key Tree") .withTabPanels( rootKeyPanel, keyTreePanel ) - ).render() - session.waitTillUserClosesSession() + ) + session.render(components) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala index e0719f62..5601b2aa 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala @@ -2,7 +2,7 @@ package org.terminal21.serverapp.bundled import org.terminal21.client.components.* import org.terminal21.client.components.frontend.ThemeToggle -import org.terminal21.client.{ConnectedSession, Controller} +import org.terminal21.client.* import org.terminal21.server.Dependencies import org.terminal21.serverapp.{ServerSideApp, ServerSideSessions} @@ -19,11 +19,11 @@ class SettingsApp extends ServerSideApp: new SettingsPage().run() class SettingsPage(using session: ConnectedSession): + import Model.unitModel val themeToggle = ThemeToggle() def run() = - components.render() controller.eventsIterator.lastOption def components = Seq(themeToggle) - def controller = Controller(()) + def controller = Controller(components) diff --git a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala index 9f1b9b73..94815e0a 100644 --- a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala +++ b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala @@ -48,7 +48,6 @@ class AppManagerPageTest extends AnyFunSuiteLike: test("starts app when app link is clicked"): val app = mockApp("app1", "the-app1-desc") new App(app): - page.components.render() val eventsIt = page.eventsIterator session.fireEvents(CommandEvent.onClick(page.appRows.head.link), CommandEvent.sessionClosed) eventsIt.toList @@ -58,7 +57,6 @@ class AppManagerPageTest extends AnyFunSuiteLike: val app = mockApp("app1", "the-app1-desc") new App(app): val other = Link() - (page.components :+ other).render() - val eventsIt = page.eventsIterator + val eventsIt = page.controller(page.components :+ other).eventsIterator session.fireEvents(CommandEvent.onClick(page.appRows.head.link), CommandEvent.onClick(other), CommandEvent.sessionClosed) eventsIt.toList.last.startApp should be(None) diff --git a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/SettingsPageTest.scala b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/SettingsPageTest.scala index 834e1ce7..46f741d4 100644 --- a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/SettingsPageTest.scala +++ b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/SettingsPageTest.scala @@ -13,11 +13,3 @@ class SettingsPageTest extends AnyFunSuiteLike: test("Should render the ThemeToggle component"): new App: page.components should contain(page.themeToggle) - - test("run() should render all components"): - new App: - fiberExecutor.submit: - page.run() - session.waitUntilAtLeast1EventIteratorWasCreated() - session.fireEvents(CommandEvent.sessionClosed) - session.currentlyRendered should be(page.components) From d78a7e2a4ec16ba719b02c3f6d1fb9a41fb72336 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 23 Feb 2024 17:04:08 +0000 Subject: [PATCH 143/313] - --- .../org/terminal21/serverapp/bundled/AppManager.scala | 10 +++------- .../serverapp/bundled/AppManagerPageTest.scala | 4 ++-- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala index bc9e6b2d..504bf1af 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala @@ -30,12 +30,8 @@ class AppManagerPage(apps: Seq[ServerSideApp], startApp: ServerSideApp => Unit)( def run(): Unit = eventsIterator.foreach(_ => ()) - case class AppRow(app: ServerSideApp, link: Link, text: Text): - def row: Seq[UiElement] = Seq(link, text) - - val appRows = apps.map: app => - AppRow( - app, + val appRows: Seq[Seq[UiElement]] = apps.map: app => + Seq( Link(text = app.name).onClick: event => import event.* handled.withModel(model.copy(startApp = Some(app))) @@ -46,7 +42,7 @@ class AppManagerPage(apps: Seq[ServerSideApp], startApp: ServerSideApp => Unit)( def components = val appsTable = QuickTable( caption = Some("Apps installed on the server, click one to run it."), - rows = appRows.map(_.row) + rows = appRows ).withHeaders("App Name", "Description") Seq( diff --git a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala index 94815e0a..e865682b 100644 --- a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala +++ b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala @@ -49,7 +49,7 @@ class AppManagerPageTest extends AnyFunSuiteLike: val app = mockApp("app1", "the-app1-desc") new App(app): val eventsIt = page.eventsIterator - session.fireEvents(CommandEvent.onClick(page.appRows.head.link), CommandEvent.sessionClosed) + session.fireEvents(CommandEvent.onClick(page.appRows.head.head), CommandEvent.sessionClosed) eventsIt.toList startedApp should be(Some(app)) @@ -58,5 +58,5 @@ class AppManagerPageTest extends AnyFunSuiteLike: new App(app): val other = Link() val eventsIt = page.controller(page.components :+ other).eventsIterator - session.fireEvents(CommandEvent.onClick(page.appRows.head.link), CommandEvent.onClick(other), CommandEvent.sessionClosed) + session.fireEvents(CommandEvent.onClick(page.appRows.head.head), CommandEvent.onClick(other), CommandEvent.sessionClosed) eventsIt.toList.last.startApp should be(None) From 9ce17c975975078eb64ceab51fce5a8ae0fb6dcd Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 23 Feb 2024 17:08:52 +0000 Subject: [PATCH 144/313] - --- .../scala/org/terminal21/serverapp/bundled/AppManager.scala | 5 ++++- .../org/terminal21/serverapp/bundled/ServerStatusApp.scala | 5 ++++- .../scala/org/terminal21/serverapp/bundled/SettingsApp.scala | 4 +++- .../src/main/scala/org/terminal21/client/Controller.scala | 4 ++++ 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala index 504bf1af..64400ae1 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala @@ -70,7 +70,10 @@ class AppManagerPage(apps: Seq[ServerSideApp], startApp: ServerSideApp => Unit)( // for every event, reset the model handled.withModel(model.copy(startApp = None)) - def controller: Controller[ManagerModel] = controller(components) + def controller: Controller[ManagerModel] = + val c = controller(components) + c.render() + c def eventsIterator: Iterator[ManagerModel] = controller.eventsIterator diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 5ea2dd12..3a7b41da 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -24,7 +24,10 @@ class ServerStatusPage( sessionsService: ServerSessionsService )(using session: ConnectedSession): import Model.unitModel - def run(): Unit = controller(Runtime.getRuntime, sessionsService.allSessions).eventsIterator.lastOption + def run(): Unit = + val c = controller(Runtime.getRuntime, sessionsService.allSessions) + c.render() + c.eventsIterator.lastOption private def toMb(v: Long) = s"${v / (1024 * 1024)} MB" private val xs = Some("2xs") diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala index 5601b2aa..6621bb5d 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala @@ -22,7 +22,9 @@ class SettingsPage(using session: ConnectedSession): import Model.unitModel val themeToggle = ThemeToggle() def run() = - controller.eventsIterator.lastOption + val c = controller + c.render() + c.eventsIterator.lastOption def components = Seq(themeToggle) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 68cf7ce4..f2f4c2c9 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -14,6 +14,10 @@ class Controller[M]( initialModel: Model[M], eventHandlers: Seq[ControllerEvent[M] => HandledEvent[M]] ): + def render()(using session: ConnectedSession): this.type = + session.render(initialComponents) + this + def onEvent(handler: ControllerEvent[M] => HandledEvent[M]) = new Controller( eventIteratorFactory, From ea857b59f4178c2ed7d6a669652d57d166d4733f Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 23 Feb 2024 17:17:48 +0000 Subject: [PATCH 145/313] - --- .../org/terminal21/serverapp/bundled/AppManager.scala | 8 ++++---- .../terminal21/serverapp/bundled/ServerStatusApp.scala | 4 +--- .../org/terminal21/serverapp/bundled/SettingsApp.scala | 4 +--- .../src/main/scala/org/terminal21/client/Controller.scala | 1 + 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala index 64400ae1..c119f417 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala @@ -71,12 +71,12 @@ class AppManagerPage(apps: Seq[ServerSideApp], startApp: ServerSideApp => Unit)( handled.withModel(model.copy(startApp = None)) def controller: Controller[ManagerModel] = - val c = controller(components) - c.render() - c + controller(components) def eventsIterator: Iterator[ManagerModel] = - controller.eventsIterator + controller + .render() + .eventsIterator .tapEach: m => for app <- m.startApp do startApp(app) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 3a7b41da..cfa3b05f 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -25,9 +25,7 @@ class ServerStatusPage( )(using session: ConnectedSession): import Model.unitModel def run(): Unit = - val c = controller(Runtime.getRuntime, sessionsService.allSessions) - c.render() - c.eventsIterator.lastOption + controller(Runtime.getRuntime, sessionsService.allSessions).render().eventsIterator.lastOption private def toMb(v: Long) = s"${v / (1024 * 1024)} MB" private val xs = Some("2xs") diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala index 6621bb5d..21ab327e 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala @@ -22,9 +22,7 @@ class SettingsPage(using session: ConnectedSession): import Model.unitModel val themeToggle = ThemeToggle() def run() = - val c = controller - c.render() - c.eventsIterator.lastOption + controller.render().eventsIterator.lastOption def components = Seq(themeToggle) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index f2f4c2c9..64f64c41 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -27,6 +27,7 @@ class Controller[M]( eventHandlers :+ handler ) + def lastEventOption: Option[M] = eventsIterator.lastOption def eventsIterator: EventIterator[M] = new EventIterator(handledEventsIterator.takeWhile(!_.shouldTerminate).map(_.model)) private def clickHandlersMap(h: HandledEvent[M]): Map[String, Seq[OnClickEventHandlerFunction[M]]] = From d9af72412bbdc0b9451c6caf6d4b434d42c8ea4c Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 23 Feb 2024 17:24:22 +0000 Subject: [PATCH 146/313] - --- .../org/terminal21/serverapp/bundled/ServerStatusApp.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index cfa3b05f..c62d71ff 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -75,7 +75,6 @@ class ServerStatusPage( .onClick: event => serverSideSessions .withNewSession(session.id + "-server-state", s"Server State:${session.id}") - .andOptions(SessionOptions.LeaveOpenWhenTerminated) .connect: sSession => new ViewServerStatePage(using sSession).runFor(sessionsService.sessionStateOf(session)) event.handled @@ -114,3 +113,4 @@ class ViewServerStatePage(using session: ConnectedSession): ) ) session.render(components) + session.leaveSessionOpenAfterExiting() From 41ffbc5341b0a3d1e4ed6e8598a49952df2f5bac Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 23 Feb 2024 17:26:39 +0000 Subject: [PATCH 147/313] - --- .../scala/org/terminal21/serverapp/bundled/AppManager.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala index c119f417..60aa8bdf 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala @@ -67,7 +67,7 @@ class AppManagerPage(apps: Seq[ServerSideApp], startApp: ServerSideApp => Unit)( Controller(components) .onEvent: event => import event.* - // for every event, reset the model + // for every event, reset the startApp so that it doesn't start the same app on each event handled.withModel(model.copy(startApp = None)) def controller: Controller[ManagerModel] = From 38c7791cf07d5b9c56e84f677939208e2172e6a7 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 23 Feb 2024 17:29:41 +0000 Subject: [PATCH 148/313] - --- .../org/terminal21/serverapp/bundled/ServerStatusApp.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index c62d71ff..76be6642 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -3,7 +3,7 @@ package org.terminal21.serverapp.bundled import org.terminal21.client.* import org.terminal21.client.components.* import org.terminal21.client.components.chakra.* -import org.terminal21.model.{Session, SessionOptions} +import org.terminal21.model.Session import org.terminal21.server.Dependencies import org.terminal21.server.model.SessionState import org.terminal21.server.service.ServerSessionsService From ac25d6f610a3f79b82fb3af015fcc07d1ce54e8f Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 23 Feb 2024 21:41:27 +0000 Subject: [PATCH 149/313] - --- build.sbt | 2 +- .../terminal21}/collections/TypedMap.scala | 2 +- .../org/terminal21/model/CommandEvent.scala | 21 ++++++++++ .../collections/TypedMapTest.scala | 3 +- .../org/terminal21/client/Controller.scala | 9 ++-- .../client/components/UiElement.scala | 2 +- .../components/chakra/ChakraElement.scala | 2 +- .../client/components/std/StdElement.scala | 2 +- .../client/components/std/StdHttp.scala | 2 +- .../client/json/UiElementEncoding.scala | 2 +- .../terminal21/client/ControllerTest.scala | 41 +++++++++++-------- 11 files changed, 59 insertions(+), 29 deletions(-) rename {terminal21-ui-std/src/main/scala/org/terminal21/client => terminal21-server-client-common/src/main/scala/org/terminal21}/collections/TypedMap.scala (91%) rename {terminal21-ui-std/src/test/scala/org/terminal21/client => terminal21-server-client-common/src/test/scala/org/terminal21}/collections/TypedMapTest.scala (89%) diff --git a/build.sbt b/build.sbt index 6a42ac9e..94390f14 100644 --- a/build.sbt +++ b/build.sbt @@ -69,7 +69,7 @@ val commonSettings = Seq( lazy val `terminal21-server-client-common` = project .settings( commonSettings, - libraryDependencies ++= Seq( + libraryDependencies ++= Circe ++ Seq( ScalaTest, Slf4jApi, HelidonClientWebSocket, diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/collections/TypedMap.scala b/terminal21-server-client-common/src/main/scala/org/terminal21/collections/TypedMap.scala similarity index 91% rename from terminal21-ui-std/src/main/scala/org/terminal21/client/collections/TypedMap.scala rename to terminal21-server-client-common/src/main/scala/org/terminal21/collections/TypedMap.scala index a375f98d..48eb3cc0 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/collections/TypedMap.scala +++ b/terminal21-server-client-common/src/main/scala/org/terminal21/collections/TypedMap.scala @@ -1,4 +1,4 @@ -package org.terminal21.client.collections +package org.terminal21.collections class TypedMap(val m: Map[TypedMapKey[_], Any]): def +[A](kv: (TypedMapKey[A], A)): TypedMap = new TypedMap(m + kv) diff --git a/terminal21-server-client-common/src/main/scala/org/terminal21/model/CommandEvent.scala b/terminal21-server-client-common/src/main/scala/org/terminal21/model/CommandEvent.scala index 949ec117..96678036 100644 --- a/terminal21-server-client-common/src/main/scala/org/terminal21/model/CommandEvent.scala +++ b/terminal21-server-client-common/src/main/scala/org/terminal21/model/CommandEvent.scala @@ -1,5 +1,8 @@ package org.terminal21.model +import io.circe.* +import io.circe.generic.auto.* +import io.circe.syntax.* import org.terminal21.client.components.AnyElement /** These are the events as they arrive from the server @@ -14,6 +17,19 @@ object CommandEvent: def onChange(receivedBy: AnyElement, value: Boolean): OnChange = OnChange(receivedBy.key, value.toString) def sessionClosed: SessionClosed = SessionClosed("-") + given Encoder[CommandEvent] = + case c: OnClick => c.asJson.mapObject(_.add("type", "OnClick".asJson)) + case c: OnChange => c.asJson.mapObject(_.add("type", "OnChange".asJson)) + case sc: SessionClosed => sc.asJson.mapObject(_.add("type", "SessionClosed".asJson)) + case x => throw new IllegalStateException(s"$x should never be send as json") + + given Decoder[CommandEvent] = o => + o.get[String]("type") match + case Right("OnClick") => o.as[OnClick] + case Right("OnChange") => o.as[OnChange] + case Right("SessionClosed") => o.as[SessionClosed] + case x => throw new IllegalStateException(s"got unexpected $x") + case class OnClick(key: String) extends CommandEvent: override def isSessionClosed: Boolean = false @@ -22,3 +38,8 @@ case class OnChange(key: String, value: String) extends CommandEvent: case class SessionClosed(key: String) extends CommandEvent: override def isSessionClosed: Boolean = true + +/** Extend this to send your own messages + */ +trait ClientEvent extends CommandEvent: + override def key = "client-event" diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/collections/TypedMapTest.scala b/terminal21-server-client-common/src/test/scala/org/terminal21/collections/TypedMapTest.scala similarity index 89% rename from terminal21-ui-std/src/test/scala/org/terminal21/client/collections/TypedMapTest.scala rename to terminal21-server-client-common/src/test/scala/org/terminal21/collections/TypedMapTest.scala index e33cd291..ef5e8547 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/collections/TypedMapTest.scala +++ b/terminal21-server-client-common/src/test/scala/org/terminal21/collections/TypedMapTest.scala @@ -1,7 +1,8 @@ -package org.terminal21.client.collections +package org.terminal21.collections import org.scalatest.funsuite.AnyFunSuiteLike import org.scalatest.matchers.should.Matchers.* +import org.terminal21.collections.{TypedMap, TypedMapKey} class TypedMapTest extends AnyFunSuiteLike: object IntKey extends TypedMapKey[Int] diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 64f64c41..35d9cb3d 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -1,14 +1,16 @@ package org.terminal21.client -import org.terminal21.client.collections.{EventIterator, TypedMapKey} +import org.terminal21.client.collections.EventIterator import org.terminal21.client.components.OnChangeEventHandler.CanHandleOnChangeEvent import org.terminal21.client.components.OnClickEventHandler.CanHandleOnClickEvent import org.terminal21.client.components.UiElement.HasEventHandler import org.terminal21.client.components.{OnChangeBooleanEventHandler, OnChangeEventHandler, OnClickEventHandler, UiElement} +import org.terminal21.collections.TypedMapKey import org.terminal21.model.{CommandEvent, OnChange, OnClick} class Controller[M]( eventIteratorFactory: => Iterator[CommandEvent], + fireEvent: CommandEvent => Unit, renderChanges: Seq[UiElement] => Unit, initialComponents: Seq[UiElement], initialModel: Model[M], @@ -21,6 +23,7 @@ class Controller[M]( def onEvent(handler: ControllerEvent[M] => HandledEvent[M]) = new Controller( eventIteratorFactory, + fireEvent, renderChanges, initialComponents, initialModel, @@ -128,9 +131,9 @@ class Controller[M]( object Controller: def apply[M](initialModel: Model[M], components: Seq[UiElement])(using session: ConnectedSession): Controller[M] = - new Controller(session.eventIterator, session.renderChanges, components, initialModel, Nil) + new Controller(session.eventIterator, session.fireEvent, session.renderChanges, components, initialModel, Nil) def apply[M](components: Seq[UiElement])(using initialModel: Model[M], session: ConnectedSession): Controller[M] = - new Controller(session.eventIterator, session.renderChanges, components, initialModel, Nil) + new Controller(session.eventIterator, session.fireEvent, session.renderChanges, components, initialModel, Nil) trait ControllerEvent[M]: def model: M = handled.model diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala index 96c6a984..ea50f7de 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala @@ -1,7 +1,7 @@ package org.terminal21.client.components import org.terminal21.client.HandledEvent -import org.terminal21.client.collections.{TypedMap, TypedMapKey} +import org.terminal21.collections.{TypedMap, TypedMapKey} trait UiElement extends AnyElement: def key: String diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala index aa747010..153b50bb 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala @@ -1,9 +1,9 @@ package org.terminal21.client.components.chakra import org.terminal21.client.ConnectedSession -import org.terminal21.client.collections.TypedMap import org.terminal21.client.components.UiElement.{HasChildren, HasEventHandler, HasStyle} import org.terminal21.client.components.* +import org.terminal21.collections.TypedMap sealed trait CEJson extends UiElement diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala index b689d7c1..fa3369b8 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala @@ -1,10 +1,10 @@ package org.terminal21.client.components.std import org.terminal21.client.components.OnChangeEventHandler.CanHandleOnChangeEvent -import org.terminal21.client.collections.TypedMap import org.terminal21.client.components.UiElement.{HasChildren, HasEventHandler, HasStyle} import org.terminal21.client.components.{Keys, OnChangeEventHandler, UiElement} import org.terminal21.client.ConnectedSession +import org.terminal21.collections.TypedMap sealed trait StdEJson extends UiElement sealed trait StdElement[A <: UiElement] extends StdEJson with HasStyle[A] diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala index ccd19b53..10c6c5a7 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala @@ -1,10 +1,10 @@ package org.terminal21.client.components.std import org.terminal21.client.components.OnChangeEventHandler.CanHandleOnChangeEvent -import org.terminal21.client.collections.TypedMap import org.terminal21.client.ConnectedSession import org.terminal21.client.components.UiElement.HasEventHandler import org.terminal21.client.components.{EventHandler, Keys, OnChangeEventHandler, TransientRequest, UiElement} +import org.terminal21.collections.TypedMap import org.terminal21.model.OnChange /** Elements mapping to Http functionality diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/json/UiElementEncoding.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/json/UiElementEncoding.scala index 7894d69b..95d4519c 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/json/UiElementEncoding.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/json/UiElementEncoding.scala @@ -3,11 +3,11 @@ package org.terminal21.client.json import io.circe.* import io.circe.generic.auto.* import io.circe.syntax.* -import org.terminal21.client.collections.TypedMap import org.terminal21.client.components.chakra.{Box, CEJson, ChakraElement} import org.terminal21.client.components.frontend.FrontEndElement import org.terminal21.client.components.std.{StdEJson, StdElement, StdHttp} import org.terminal21.client.components.{ComponentLib, UiComponent, UiElement} +import org.terminal21.collections.TypedMap class UiElementEncoding(libs: Seq[ComponentLib]): given uiElementEncoder: Encoder[UiElement] = diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 8e9ed556..e31e111b 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -5,6 +5,7 @@ import org.scalatest.matchers.should.Matchers.* import org.terminal21.client.components.UiElement import org.terminal21.client.components.chakra.{Box, Button, Checkbox} import org.terminal21.client.components.std.Input +import org.terminal21.collections.SEList import org.terminal21.model.{CommandEvent, OnChange, OnClick} class ControllerTest extends AnyFunSuiteLike: @@ -17,15 +18,19 @@ class ControllerTest extends AnyFunSuiteLike: def newController[M]( initialModel: Model[M], - eventIterator: => Iterator[CommandEvent], + events: => Seq[CommandEvent], components: Seq[UiElement], renderChanges: Seq[UiElement] => Unit = _ => () ): Controller[M] = - new Controller(eventIterator, renderChanges, components, initialModel, Nil) + val seList = SEList[CommandEvent]() + val it = seList.iterator + events.foreach(e => seList.add(e)) + seList.add(CommandEvent.sessionClosed) + new Controller(it, event => (), renderChanges, components, initialModel, Nil) test("onEvent is called"): val model = Model(0) - newController(model, Iterator(buttonClick), Seq(button)) + newController(model, Seq(buttonClick), Seq(button)) .onEvent: event => if event.model > 1 then event.handled.terminate else event.handled.withModel(event.model + 1) .eventsIterator @@ -33,7 +38,7 @@ class ControllerTest extends AnyFunSuiteLike: test("onEvent is called for change"): val model = Model(0) - newController(model, Iterator(inputChange), Seq(input)) + newController(model, Seq(inputChange), Seq(input)) .onEvent: event => import event.* if event.model > 1 then handled.terminate else handled.withModel(event.model + 1) @@ -42,7 +47,7 @@ class ControllerTest extends AnyFunSuiteLike: test("onEvent is called for change/boolean"): val model = Model(0) - newController(model, Iterator(checkBoxChange), Seq(checkbox)) + newController(model, Seq(checkBoxChange), Seq(checkbox)) .onEvent: event => import event.* if event.model > 1 then handled.terminate else handled.withModel(event.model + 1) @@ -53,7 +58,7 @@ class ControllerTest extends AnyFunSuiteLike: given model: Model[Int] = Model(0) newController( model, - Iterator(buttonClick), + Seq(buttonClick), Seq( button.onClick: event => event.handled.withModel(100).terminate @@ -64,7 +69,7 @@ class ControllerTest extends AnyFunSuiteLike: given model: Model[Int] = Model(0) newController( model, - Iterator(inputChange), + Seq(inputChange), Seq( input.onChange: event => event.handled.withModel(100).terminate @@ -75,7 +80,7 @@ class ControllerTest extends AnyFunSuiteLike: given model: Model[Int] = Model(0) newController( model, - Iterator(checkBoxChange), + Seq(checkBoxChange), Seq( checkbox.onChange: event => event.handled.withModel(100).terminate @@ -84,7 +89,7 @@ class ControllerTest extends AnyFunSuiteLike: test("terminate is obeyed and latest model state is iterated"): val model = Model(0) - newController(model, Iterator(buttonClick, buttonClick, buttonClick), Seq(button)) + newController(model, Seq(buttonClick, buttonClick, buttonClick), Seq(button)) .onEvent: event => if event.model > 1 then event.handled.terminate.withModel(100) else event.handled.withModel(event.model + 1) .eventsIterator @@ -94,7 +99,7 @@ class ControllerTest extends AnyFunSuiteLike: var rendered = Seq.empty[UiElement] def renderer(s: Seq[UiElement]): Unit = rendered = s - newController(Model(0), Iterator(buttonClick), Seq(button), renderer) + newController(Model(0), Seq(buttonClick), Seq(button), renderer) .onEvent: event => event.handled.withModel(event.model + 1).withRenderChanges(button.withText("changed")).terminate .eventsIterator @@ -109,7 +114,7 @@ class ControllerTest extends AnyFunSuiteLike: val model = Model(0) val handled = newController( model, - Iterator(buttonClick, checkBoxChange), + Seq(buttonClick, checkBoxChange), Seq( button.onClick(using model): event => event.handled.withRenderChanges(button.withText("changed")), @@ -124,7 +129,7 @@ class ControllerTest extends AnyFunSuiteLike: test("timed changes are rendered"): @volatile var rendered = Seq.empty[UiElement] def renderer(s: Seq[UiElement]): Unit = rendered = s - newController(Model(0), Iterator(buttonClick), Seq(button), renderer) + newController(Model(0), Seq(buttonClick), Seq(button), renderer) .onEvent: event => event.handled.withModel(1).withTimedRenderChanges(TimedRenderChanges(10, button.withText("changed"))).terminate .eventsIterator @@ -136,7 +141,7 @@ class ControllerTest extends AnyFunSuiteLike: val model = Model(0) newController( model, - Iterator(buttonClick), + Seq(buttonClick), Seq( button.onClick(using model): event => event.handled.withTimedRenderChanges(TimedRenderChanges(10, button.withText("changed"))).terminate @@ -149,7 +154,7 @@ class ControllerTest extends AnyFunSuiteLike: event.handled.withModel(2) newController( model, - Iterator(buttonClick, checkBoxChange), + Seq(buttonClick, checkBoxChange), Seq( button.onClick(using model): event => event.handled.withTimedRenderChanges(TimedRenderChanges(10, c)) @@ -160,7 +165,7 @@ class ControllerTest extends AnyFunSuiteLike: val model = Model(0) newController( model, - Iterator(inputChange), + Seq(inputChange), Seq( input.onChange(using model): event => import event.* @@ -172,7 +177,7 @@ class ControllerTest extends AnyFunSuiteLike: val model = Model(0) newController( model, - Iterator(checkBoxChange), + Seq(checkBoxChange), Seq( checkbox.onChange(using model): event => import event.* @@ -187,7 +192,7 @@ class ControllerTest extends AnyFunSuiteLike: event.handled.withRenderChanges(box.withChildren(button, checkbox)) ) - val handledEvents = newController(model, Iterator(buttonClick), Seq(box)).handledEventsIterator.toList + val handledEvents = newController(model, Seq(buttonClick), Seq(box)).handledEventsIterator.toList handledEvents(1).componentsByKey(checkbox.key) should be(checkbox) test("newly rendered elements event handlers are invoked"): @@ -205,4 +210,4 @@ class ControllerTest extends AnyFunSuiteLike: lazy val box: Box = Box().withChildren(b) - newController(model, Iterator(buttonClick, checkBoxChange), Seq(box)).eventsIterator.toList should be(List(0, 1, 2)) + newController(model, Seq(buttonClick, checkBoxChange), Seq(box)).eventsIterator.toList should be(List(0, 1, 2)) From bdda0b22155b798d80aefde1db53ff3bec4b5fe4 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 23 Feb 2024 22:57:45 +0000 Subject: [PATCH 150/313] - --- .../org/terminal21/model/CommandEvent.scala | 3 ++- .../org/terminal21/client/Controller.scala | 24 ++++++++++++------- .../terminal21/client/ControllerTest.scala | 15 +++++++++++- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/terminal21-server-client-common/src/main/scala/org/terminal21/model/CommandEvent.scala b/terminal21-server-client-common/src/main/scala/org/terminal21/model/CommandEvent.scala index 96678036..55590a8f 100644 --- a/terminal21-server-client-common/src/main/scala/org/terminal21/model/CommandEvent.scala +++ b/terminal21-server-client-common/src/main/scala/org/terminal21/model/CommandEvent.scala @@ -42,4 +42,5 @@ case class SessionClosed(key: String) extends CommandEvent: /** Extend this to send your own messages */ trait ClientEvent extends CommandEvent: - override def key = "client-event" + override def key = "client-event" + override def isSessionClosed: Boolean = false diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 35d9cb3d..a58650ad 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -6,7 +6,7 @@ import org.terminal21.client.components.OnClickEventHandler.CanHandleOnClickEven import org.terminal21.client.components.UiElement.HasEventHandler import org.terminal21.client.components.{OnChangeBooleanEventHandler, OnChangeEventHandler, OnClickEventHandler, UiElement} import org.terminal21.collections.TypedMapKey -import org.terminal21.model.{CommandEvent, OnChange, OnClick} +import org.terminal21.model.{ClientEvent, CommandEvent, OnChange, OnClick} class Controller[M]( eventIteratorFactory: => Iterator[CommandEvent], @@ -58,13 +58,16 @@ class Controller[M]( .withDefault(key => throw new IllegalArgumentException(s"Component with key=$key is not available")) private def updateComponentsFromEvent(handled: HandledEvent[M], event: CommandEvent): HandledEvent[M] = - handled.componentsByKey(event.key) match - case e: UiElement with HasEventHandler[_] => - event match - case OnChange(key, value) => - handled.copy(componentsByKey = handled.componentsByKey + (key -> e.defaultEventHandler(value))) - case _ => handled - case _ => handled + event match + case _: ClientEvent => handled + case _ => + handled.componentsByKey(event.key) match + case e: UiElement with HasEventHandler[_] => + event match + case OnChange(key, value) => + handled.copy(componentsByKey = handled.componentsByKey + (key -> e.defaultEventHandler(value))) + case _ => handled + case _ => handled private def invokeEventHandlers(handled: HandledEvent[M], event: CommandEvent): HandledEvent[M] = eventHandlers.foldLeft(handled.copy(renderChanges = Nil, timedRenderChanges = Nil)): (h, f) => @@ -77,6 +80,8 @@ class Controller[M]( case _: OnChangeEventHandler.CanHandleOnChangeEvent[_] => ControllerChangeEvent(receivedBy, h, value) case _: OnChangeBooleanEventHandler.CanHandleOnChangeEvent[_] => ControllerChangeBooleanEvent(receivedBy, h, value.toBoolean) f(e) + case ce: ClientEvent => + f(ControllerClientEvent(handled, ce)) case x => throw new IllegalStateException(s"Unexpected state $x") private def invokeComponentEventHandlers(h: HandledEvent[M], event: CommandEvent) = @@ -135,7 +140,7 @@ object Controller: def apply[M](components: Seq[UiElement])(using initialModel: Model[M], session: ConnectedSession): Controller[M] = new Controller(session.eventIterator, session.fireEvent, session.renderChanges, components, initialModel, Nil) -trait ControllerEvent[M]: +sealed trait ControllerEvent[M]: def model: M = handled.model def handled: HandledEvent[M] extension [A <: UiElement](e: A) def current: A = handled.current(e) @@ -143,6 +148,7 @@ trait ControllerEvent[M]: case class ControllerClickEvent[M](clicked: UiElement, handled: HandledEvent[M]) extends ControllerEvent[M] case class ControllerChangeEvent[M](changed: UiElement, handled: HandledEvent[M], newValue: String) extends ControllerEvent[M] case class ControllerChangeBooleanEvent[M](changed: UiElement, handled: HandledEvent[M], newValue: Boolean) extends ControllerEvent[M] +case class ControllerClientEvent[M](handled: HandledEvent[M], event: ClientEvent) extends ControllerEvent[M] case class HandledEvent[M]( model: M, diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index e31e111b..2ce1f3d1 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -6,7 +6,7 @@ import org.terminal21.client.components.UiElement import org.terminal21.client.components.chakra.{Box, Button, Checkbox} import org.terminal21.client.components.std.Input import org.terminal21.collections.SEList -import org.terminal21.model.{CommandEvent, OnChange, OnClick} +import org.terminal21.model.{ClientEvent, CommandEvent, OnChange, OnClick} class ControllerTest extends AnyFunSuiteLike: val button = Button() @@ -54,6 +54,19 @@ class ControllerTest extends AnyFunSuiteLike: .eventsIterator .toList should be(List(0, 1)) + case class TestClientEvent(i: Int) extends ClientEvent + + test("onEvent is called for ClientEvent"): + val model = Model(0) + newController(model, Seq(TestClientEvent(5)), Seq(button)) + .onEvent: + case ControllerClientEvent(handled, event: TestClientEvent) => + import event.* + handled.withModel(event.i).terminate + case event => event.handled + .eventsIterator + .toList should be(List(0, 5)) + test("onClick is called"): given model: Model[Int] = Model(0) newController( From d42b6877ed7c792fd50692835ddd25e2187ac25c Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Sat, 24 Feb 2024 19:18:23 +0000 Subject: [PATCH 151/313] - --- .../serverapp/bundled/ServerStatusApp.scala | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 76be6642..1362966b 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -34,30 +34,32 @@ class ServerStatusPage( Controller(components(runtime, sessions)) def components(runtime: Runtime, sessions: Seq[Session]): Seq[UiElement] = - val jvmTable = QuickTable(caption = Some("JVM")) - .withHeaders("Property", "Value", "Actions") - .withRows( + Seq(jvmTable(runtime), sessionsTable(sessions)) + + def jvmTable(runtime: Runtime) = QuickTable(caption = Some("JVM")) + .withHeaders("Property", "Value", "Actions") + .withRows( + Seq( + Seq("Free Memory", toMb(runtime.freeMemory()), ""), + Seq("Max Memory", toMb(runtime.maxMemory()), ""), Seq( - Seq("Free Memory", toMb(runtime.freeMemory()), ""), - Seq("Max Memory", toMb(runtime.maxMemory()), ""), - Seq( - "Total Memory", - toMb(runtime.totalMemory()), - Button(size = xs, text = "Run GC").onClick: event => - System.gc() - event.handled - ), - Seq("Available processors", runtime.availableProcessors(), "") - ) + "Total Memory", + toMb(runtime.totalMemory()), + Button(size = xs, text = "Run GC").onClick: event => + System.gc() + event.handled + ), + Seq("Available processors", runtime.availableProcessors(), "") ) - val sessionsTable = QuickTable( - caption = Some("All sessions"), - rows = sessions.map: session => - Seq(Text(text = session.id), Text(text = session.name), if session.isOpen then CheckIcon() else NotAllowedIcon(), actionsFor(session)) ) - .withHeaders("Id", "Name", "Is Open", "Actions") - Seq(jvmTable, sessionsTable) + def sessionsTable(sessions: Seq[Session]) = QuickTable( + key = "sessions-table", + caption = Some("All sessions"), + rows = sessions.map: session => + Seq(Text(text = session.id), Text(text = session.name), if session.isOpen then CheckIcon() else NotAllowedIcon(), actionsFor(session)) + ) + .withHeaders("Id", "Name", "Is Open", "Actions") private def actionsFor(session: Session): UiElement = if session.isOpen then From 5b2db42f80675e4594ea3a0da13cf99f820b8357 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Sun, 25 Feb 2024 01:24:12 +0000 Subject: [PATCH 152/313] - --- .../org/terminal21/serverapp/bundled/ServerStatusApp.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 1362966b..9862f66b 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -22,7 +22,7 @@ class ServerStatusApp extends ServerSideApp: class ServerStatusPage( serverSideSessions: ServerSideSessions, sessionsService: ServerSessionsService -)(using session: ConnectedSession): +)(using appSession: ConnectedSession): import Model.unitModel def run(): Unit = controller(Runtime.getRuntime, sessionsService.allSessions).render().eventsIterator.lastOption From adf16cab3868180c3fa405eed756db1b5b91930a Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Sun, 25 Feb 2024 13:33:10 +0000 Subject: [PATCH 153/313] - --- .../org/terminal21/client/Controller.scala | 14 ++++---- .../client/components/EventHandler.scala | 21 +++++++----- .../client/components/UiElement.scala | 14 ++++---- .../components/chakra/ChakraElement.scala | 33 ++++++++++++------- .../client/components/std/StdElement.scala | 3 +- .../client/components/std/StdHttp.scala | 3 +- 6 files changed, 53 insertions(+), 35 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index a58650ad..078f5e7c 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -36,17 +36,17 @@ class Controller[M]( private def clickHandlersMap(h: HandledEvent[M]): Map[String, Seq[OnClickEventHandlerFunction[M]]] = h.componentsByKey.values .collect: - case e: OnClickEventHandler.CanHandleOnClickEvent[_] if e.dataStore.contains(initialModel.ClickKey) => (e.key, e.dataStore(initialModel.ClickKey)) + case e: OnClickEventHandler.CanHandleOnClickEvent if e.dataStore.contains(initialModel.ClickKey) => (e.key, e.dataStore(initialModel.ClickKey)) .toMap private def changeHandlersMap(h: HandledEvent[M]): Map[String, Seq[OnChangeEventHandlerFunction[M]]] = h.componentsByKey.values .collect: - case e: OnChangeEventHandler.CanHandleOnChangeEvent[_] if e.dataStore.contains(initialModel.ChangeKey) => (e.key, e.dataStore(initialModel.ChangeKey)) + case e: OnChangeEventHandler.CanHandleOnChangeEvent if e.dataStore.contains(initialModel.ChangeKey) => (e.key, e.dataStore(initialModel.ChangeKey)) .toMap private def changeBooleanHandlersMap(h: HandledEvent[M]): Map[String, Seq[OnChangeBooleanEventHandlerFunction[M]]] = h.componentsByKey.values .collect: - case e: OnChangeBooleanEventHandler.CanHandleOnChangeEvent[_] if e.dataStore.contains(initialModel.ChangeBooleanKey) => + case e: OnChangeBooleanEventHandler.CanHandleOnChangeEvent if e.dataStore.contains(initialModel.ChangeBooleanKey) => (e.key, e.dataStore(initialModel.ChangeBooleanKey)) .toMap @@ -62,12 +62,12 @@ class Controller[M]( case _: ClientEvent => handled case _ => handled.componentsByKey(event.key) match - case e: UiElement with HasEventHandler[_] => + case e: UiElement with HasEventHandler => event match case OnChange(key, value) => handled.copy(componentsByKey = handled.componentsByKey + (key -> e.defaultEventHandler(value))) case _ => handled - case _ => handled + case _ => handled private def invokeEventHandlers(handled: HandledEvent[M], event: CommandEvent): HandledEvent[M] = eventHandlers.foldLeft(handled.copy(renderChanges = Nil, timedRenderChanges = Nil)): (h, f) => @@ -77,8 +77,8 @@ class Controller[M]( case OnChange(key, value) => val receivedBy = h.componentsByKey(key) val e = receivedBy match - case _: OnChangeEventHandler.CanHandleOnChangeEvent[_] => ControllerChangeEvent(receivedBy, h, value) - case _: OnChangeBooleanEventHandler.CanHandleOnChangeEvent[_] => ControllerChangeBooleanEvent(receivedBy, h, value.toBoolean) + case _: OnChangeEventHandler.CanHandleOnChangeEvent => ControllerChangeEvent(receivedBy, h, value) + case _: OnChangeBooleanEventHandler.CanHandleOnChangeEvent => ControllerChangeBooleanEvent(receivedBy, h, value.toBoolean) f(e) case ce: ClientEvent => f(ControllerClientEvent(handled, ce)) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala index 0e1f1333..06ca2b8c 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala @@ -6,22 +6,25 @@ import org.terminal21.client.{Model, OnChangeBooleanEventHandlerFunction, OnChan trait EventHandler object OnClickEventHandler: - trait CanHandleOnClickEvent[A <: UiElement] extends HasDataStore[A]: - this: A => - def onClick[M](using model: Model[M])(h: OnClickEventHandlerFunction[M]): A = + trait CanHandleOnClickEvent extends HasDataStore: + this: UiElement => + type This <: UiElement + def onClick[M](using model: Model[M])(h: OnClickEventHandlerFunction[M]): This = val handlers = dataStore.getOrElse(model.ClickKey, Nil) store(model.ClickKey, handlers :+ h) object OnChangeEventHandler: - trait CanHandleOnChangeEvent[A <: UiElement] extends HasDataStore[A] with HasEventHandler[A]: - this: A => - def onChange[M](using model: Model[M])(h: OnChangeEventHandlerFunction[M]): A = + trait CanHandleOnChangeEvent extends HasDataStore with HasEventHandler: + this: UiElement => + type This <: UiElement + def onChange[M](using model: Model[M])(h: OnChangeEventHandlerFunction[M]): This = val handlers = dataStore.getOrElse(model.ChangeKey, Nil) store(model.ChangeKey, handlers :+ h) object OnChangeBooleanEventHandler: - trait CanHandleOnChangeEvent[A <: UiElement] extends HasDataStore[A] with HasEventHandler[A]: - this: A => - def onChange[M](using model: Model[M])(h: OnChangeBooleanEventHandlerFunction[M]): A = + trait CanHandleOnChangeEvent extends HasDataStore with HasEventHandler: + this: UiElement => + type This <: UiElement + def onChange[M](using model: Model[M])(h: OnChangeBooleanEventHandlerFunction[M]): This = val handlers = dataStore.getOrElse(model.ChangeBooleanKey, Nil) store(model.ChangeBooleanKey, handlers :+ h) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala index ea50f7de..601f0b88 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala @@ -20,16 +20,18 @@ object UiElement: def noChildren: A = withChildren() def addChildren(cn: UiElement*): A = withChildren(children ++ cn: _*) - trait HasEventHandler[A <: UiElement]: - def defaultEventHandler: String => A + trait HasEventHandler: + type This <: UiElement + def defaultEventHandler: String => This trait HasStyle[A <: UiElement]: def style: Map[String, Any] def withStyle(v: Map[String, Any]): A def withStyle(vs: (String, Any)*): A = withStyle(vs.toMap) - trait HasDataStore[A <: UiElement]: - this: A => + trait HasDataStore: + this: UiElement => + type This <: UiElement def dataStore: TypedMap - def withDataStore(ds: TypedMap): A - def store[V](key: TypedMapKey[V], value: V): A = withDataStore(dataStore + (key -> value)) + def withDataStore(ds: TypedMap): This + def store[V](key: TypedMapKey[V], value: V): This = withDataStore(dataStore + (key -> value)) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala index 153b50bb..e53155d9 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala @@ -31,7 +31,8 @@ case class Button( spacing: Option[String] = None, dataStore: TypedMap = TypedMap.empty ) extends ChakraElement[Button] - with OnClickEventHandler.CanHandleOnClickEvent[Button]: + with OnClickEventHandler.CanHandleOnClickEvent: + type This = Button override def withStyle(v: Map[String, Any]): Button = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) @@ -158,7 +159,8 @@ case class Editable( dataStore: TypedMap = TypedMap.empty ) extends ChakraElement[Editable] with HasChildren[Editable] - with OnChangeEventHandler.CanHandleOnChangeEvent[Editable]: + with OnChangeEventHandler.CanHandleOnChangeEvent: + type This = Editable override def defaultEventHandler = newValue => copy(valueReceived = Some(newValue)) override def withChildren(cn: UiElement*) = copy(children = cn) @@ -235,7 +237,8 @@ case class Input( style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty ) extends ChakraElement[Input] - with OnChangeEventHandler.CanHandleOnChangeEvent[Input]: + with OnChangeEventHandler.CanHandleOnChangeEvent: + type This = Input override def defaultEventHandler = newValue => copy(valueReceived = Some(newValue)) override def withStyle(v: Map[String, Any]): Input = copy(style = v) def withKey(v: String): Input = copy(key = v) @@ -294,7 +297,8 @@ case class Checkbox( checkedV: Option[Boolean] = None, dataStore: TypedMap = TypedMap.empty ) extends ChakraElement[Checkbox] - with OnChangeBooleanEventHandler.CanHandleOnChangeEvent[Checkbox]: + with OnChangeBooleanEventHandler.CanHandleOnChangeEvent: + type This = Checkbox def checked: Boolean = checkedV.getOrElse(defaultChecked) override def defaultEventHandler = newValue => copy(checkedV = Some(newValue.toBoolean)) override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -328,7 +332,8 @@ case class RadioGroup( dataStore: TypedMap = TypedMap.empty ) extends ChakraElement[RadioGroup] with HasChildren[RadioGroup] - with OnChangeEventHandler.CanHandleOnChangeEvent[RadioGroup]: + with OnChangeEventHandler.CanHandleOnChangeEvent: + type This = RadioGroup override def defaultEventHandler = newValue => copy(valueReceived = Some(newValue)) override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1398,7 +1403,8 @@ case class Textarea( style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty ) extends ChakraElement[Textarea] - with OnChangeEventHandler.CanHandleOnChangeEvent[Textarea]: + with OnChangeEventHandler.CanHandleOnChangeEvent: + type This = Textarea override def defaultEventHandler = newValue => copy(valueReceived = Some(newValue)) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1421,7 +1427,8 @@ case class Switch( checkedV: Option[Boolean] = None, // use checked dataStore: TypedMap = TypedMap.empty ) extends ChakraElement[Switch] - with OnChangeBooleanEventHandler.CanHandleOnChangeEvent[Switch]: + with OnChangeBooleanEventHandler.CanHandleOnChangeEvent: + type This = Switch def checked: Boolean = checkedV.getOrElse(defaultChecked) override def defaultEventHandler = newValue => copy(checkedV = Some(newValue.toBoolean)) override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1446,7 +1453,8 @@ case class Select( dataStore: TypedMap = TypedMap.empty ) extends ChakraElement[Select] with HasChildren[Select] - with OnChangeEventHandler.CanHandleOnChangeEvent[Select]: + with OnChangeEventHandler.CanHandleOnChangeEvent: + type This = Select override def defaultEventHandler = newValue => copy(valueReceived = Some(newValue)) override def withStyle(v: Map[String, Any]) = copy(style = v) override def withChildren(cn: UiElement*) = copy(children = cn) @@ -1615,7 +1623,8 @@ case class MenuItem( dataStore: TypedMap = TypedMap.empty ) extends ChakraElement[MenuItem] with HasChildren[MenuItem] - with OnClickEventHandler.CanHandleOnClickEvent[MenuItem]: + with OnClickEventHandler.CanHandleOnClickEvent: + type This = MenuItem override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1949,7 +1958,8 @@ case class BreadcrumbLink( dataStore: TypedMap = TypedMap.empty ) extends ChakraElement[BreadcrumbLink] with HasChildren[BreadcrumbLink] - with OnClickEventHandler.CanHandleOnClickEvent[BreadcrumbLink]: + with OnClickEventHandler.CanHandleOnClickEvent: + type This = BreadcrumbLink def withKey(v: String) = copy(key = v) override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1968,7 +1978,8 @@ case class Link( dataStore: TypedMap = TypedMap.empty ) extends ChakraElement[Link] with HasChildren[Link] - with OnClickEventHandler.CanHandleOnClickEvent[Link]: + with OnClickEventHandler.CanHandleOnClickEvent: + type This = Link def withKey(v: String) = copy(key = v) override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala index fa3369b8..11fb311d 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala @@ -73,7 +73,8 @@ case class Input( valueReceived: Option[String] = None, // use value instead dataStore: TypedMap = TypedMap.empty ) extends StdElement[Input] - with CanHandleOnChangeEvent[Input]: + with CanHandleOnChangeEvent: + type This = Input override def defaultEventHandler = newValue => copy(valueReceived = Some(newValue)) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala index 10c6c5a7..0d91f73b 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala @@ -43,6 +43,7 @@ case class CookieReader( requestId: String = TransientRequest.newRequestId(), dataStore: TypedMap = TypedMap.empty ) extends StdHttp - with CanHandleOnChangeEvent[CookieReader]: + with CanHandleOnChangeEvent: + type This = CookieReader override def defaultEventHandler = newValue => copy(value = Some(newValue)) override def withDataStore(ds: TypedMap): CookieReader = copy(dataStore = ds) From 61f186064595ecb9bb6bfe7c78059f5546529014 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Sun, 25 Feb 2024 13:48:52 +0000 Subject: [PATCH 154/313] - --- .../main/scala/org/terminal21/client/components/UiElement.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala index 601f0b88..794149b5 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala @@ -1,6 +1,5 @@ package org.terminal21.client.components -import org.terminal21.client.HandledEvent import org.terminal21.collections.{TypedMap, TypedMapKey} trait UiElement extends AnyElement: From 04610edc7581e820e320b79c08707ed8b7135430 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Sun, 25 Feb 2024 14:11:53 +0000 Subject: [PATCH 155/313] - --- .../serverapp/bundled/ServerStatusApp.scala | 2 +- .../org/terminal21/client/Controller.scala | 12 ++++---- .../terminal21/client/ControllerTest.scala | 30 ++++++++++++++++++- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 9862f66b..08c95ba8 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -31,7 +31,7 @@ class ServerStatusPage( private val xs = Some("2xs") def controller(runtime: Runtime, sessions: Seq[Session]): Controller[Unit] = - Controller(components(runtime, sessions)) + Controller(components(runtime, sessions)) // .onEvent() def components(runtime: Runtime, sessions: Seq[Session]): Seq[UiElement] = Seq(jvmTable(runtime), sessionsTable(sessions)) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 078f5e7c..7a8de601 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -14,13 +14,13 @@ class Controller[M]( renderChanges: Seq[UiElement] => Unit, initialComponents: Seq[UiElement], initialModel: Model[M], - eventHandlers: Seq[ControllerEvent[M] => HandledEvent[M]] + eventHandlers: Seq[PartialFunction[ControllerEvent[M], HandledEvent[M]]] ): def render()(using session: ConnectedSession): this.type = session.render(initialComponents) this - def onEvent(handler: ControllerEvent[M] => HandledEvent[M]) = + def onEvent(handler: PartialFunction[ControllerEvent[M], HandledEvent[M]]) = new Controller( eventIteratorFactory, fireEvent, @@ -73,15 +73,17 @@ class Controller[M]( eventHandlers.foldLeft(handled.copy(renderChanges = Nil, timedRenderChanges = Nil)): (h, f) => event match case OnClick(key) => - f(ControllerClickEvent(h.componentsByKey(key), h)) + val e = ControllerClickEvent(h.componentsByKey(key), h) + if f.isDefinedAt(e) then f(e) else h case OnChange(key, value) => val receivedBy = h.componentsByKey(key) val e = receivedBy match case _: OnChangeEventHandler.CanHandleOnChangeEvent => ControllerChangeEvent(receivedBy, h, value) case _: OnChangeBooleanEventHandler.CanHandleOnChangeEvent => ControllerChangeBooleanEvent(receivedBy, h, value.toBoolean) - f(e) + if f.isDefinedAt(e) then f(e) else h case ce: ClientEvent => - f(ControllerClientEvent(handled, ce)) + val e = ControllerClientEvent(handled, ce) + if f.isDefinedAt(e) then f(e) else h case x => throw new IllegalStateException(s"Unexpected state $x") private def invokeComponentEventHandlers(h: HandledEvent[M], event: CommandEvent) = diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 2ce1f3d1..f2f1e3a0 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -45,6 +45,16 @@ class ControllerTest extends AnyFunSuiteLike: .eventsIterator .toList should be(List(0, 1)) + test("onEvent not matched for change"): + val model = Model(0) + newController(model, Seq(inputChange), Seq(input)) + .onEvent: + case event: ControllerClickEvent[_] => + import event.* + handled.withModel(5) + .eventsIterator + .toList should be(List(0, 0)) + test("onEvent is called for change/boolean"): val model = Model(0) newController(model, Seq(checkBoxChange), Seq(checkbox)) @@ -54,6 +64,16 @@ class ControllerTest extends AnyFunSuiteLike: .eventsIterator .toList should be(List(0, 1)) + test("onEvent not matches for change/boolean"): + val model = Model(0) + newController(model, Seq(checkBoxChange), Seq(checkbox)) + .onEvent: + case event: ControllerClickEvent[_] => + import event.* + handled.withModel(5) + .eventsIterator + .toList should be(List(0, 0)) + case class TestClientEvent(i: Int) extends ClientEvent test("onEvent is called for ClientEvent"): @@ -63,10 +83,18 @@ class ControllerTest extends AnyFunSuiteLike: case ControllerClientEvent(handled, event: TestClientEvent) => import event.* handled.withModel(event.i).terminate - case event => event.handled .eventsIterator .toList should be(List(0, 5)) + test("onEvent when no partial function matches ClientEvent"): + val model = Model(0) + newController(model, Seq(TestClientEvent(5)), Seq(button)) + .onEvent: + case ControllerClickEvent(`checkbox`, handled) => + handled.withModel(5).terminate + .eventsIterator + .toList should be(List(0, 0)) + test("onClick is called"): given model: Model[Int] = Model(0) newController( From 0321986385b40c9899a917767fa0af6f6225eae7 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Sun, 25 Feb 2024 14:33:09 +0000 Subject: [PATCH 156/313] - --- .../serverapp/bundled/ServerStatusApp.scala | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 08c95ba8..7c570d2f 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -1,9 +1,10 @@ package org.terminal21.serverapp.bundled +import functions.fibers.FiberExecutor import org.terminal21.client.* import org.terminal21.client.components.* import org.terminal21.client.components.chakra.* -import org.terminal21.model.Session +import org.terminal21.model.{ClientEvent, Session} import org.terminal21.server.Dependencies import org.terminal21.server.model.SessionState import org.terminal21.server.service.ServerSessionsService @@ -17,21 +18,31 @@ class ServerStatusApp extends ServerSideApp: serverSideSessions .withNewSession("server-status", "Server Status") .connect: session => - new ServerStatusPage(serverSideSessions, dependencies.sessionsService)(using session).run() + new ServerStatusPage(serverSideSessions, dependencies.sessionsService)(using session, dependencies.fiberExecutor).run() class ServerStatusPage( serverSideSessions: ServerSideSessions, sessionsService: ServerSessionsService -)(using appSession: ConnectedSession): +)(using appSession: ConnectedSession, fiberExecutor: FiberExecutor): import Model.unitModel + + case object Ticker extends ClientEvent + def run(): Unit = + fiberExecutor.submit: + while !appSession.isClosed do + Thread.sleep(2000) + appSession.fireEvents(Ticker) controller(Runtime.getRuntime, sessionsService.allSessions).render().eventsIterator.lastOption private def toMb(v: Long) = s"${v / (1024 * 1024)} MB" private val xs = Some("2xs") def controller(runtime: Runtime, sessions: Seq[Session]): Controller[Unit] = - Controller(components(runtime, sessions)) // .onEvent() + Controller(components(runtime, sessions)).onEvent: + case ControllerClientEvent(handled, Ticker) => + println("Ticker") + handled.withRenderChanges(sessionsTable(sessionsService.allSessions)) def components(runtime: Runtime, sessions: Seq[Session]): Seq[UiElement] = Seq(jvmTable(runtime), sessionsTable(sessions)) From b9160b700c09f8f30a2d30c6d1f28e32e23fc8b9 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Sun, 25 Feb 2024 15:07:34 +0000 Subject: [PATCH 157/313] - --- .../org/terminal21/serverapp/bundled/ServerStatusApp.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 7c570d2f..dc88657d 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -41,13 +41,12 @@ class ServerStatusPage( def controller(runtime: Runtime, sessions: Seq[Session]): Controller[Unit] = Controller(components(runtime, sessions)).onEvent: case ControllerClientEvent(handled, Ticker) => - println("Ticker") handled.withRenderChanges(sessionsTable(sessionsService.allSessions)) def components(runtime: Runtime, sessions: Seq[Session]): Seq[UiElement] = Seq(jvmTable(runtime), sessionsTable(sessions)) - def jvmTable(runtime: Runtime) = QuickTable(caption = Some("JVM")) + def jvmTable(runtime: Runtime) = QuickTable(key = "jvmTable", caption = Some("JVM")) .withHeaders("Property", "Value", "Actions") .withRows( Seq( From 7ee3019771dcf09251c1afa257a9d62218a3a32d Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Sun, 25 Feb 2024 15:12:46 +0000 Subject: [PATCH 158/313] - --- .../main/scala/org/terminal21/client/ConnectedSession.scala | 4 ---- 1 file changed, 4 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index 9a5c5922..d4018ade 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -1,8 +1,6 @@ package org.terminal21.client import io.circe.* -import io.circe.generic.auto.* -import org.slf4j.LoggerFactory import org.terminal21.client.components.UiElement.HasChildren import org.terminal21.client.components.{UiComponent, UiElement} import org.terminal21.client.json.UiElementEncoding @@ -13,10 +11,8 @@ import org.terminal21.ui.std.{ServerJson, SessionsService} import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.{CountDownLatch, TimeUnit} import scala.annotation.tailrec -import scala.collection.concurrent.TrieMap class ConnectedSession(val session: Session, encoding: UiElementEncoding, val serverUrl: String, sessionsService: SessionsService, onCloseHandler: () => Unit): - private val logger = LoggerFactory.getLogger(getClass) @volatile private var events = SEList[CommandEvent]() def uiUrl: String = serverUrl + "/ui" From 973f15c90bfa8a0fed7f370c585307365ad25eda Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Sun, 25 Feb 2024 16:19:41 +0000 Subject: [PATCH 159/313] - --- .../org/terminal21/ui/std/ServerJson.scala | 15 ++++++++++++--- .../org/terminal21/ui/std/ServerJsonTest.scala | 18 +++++++++++++++--- .../terminal21/client/ConnectedSession.scala | 1 - .../org/terminal21/client/Controller.scala | 6 +++--- 4 files changed, 30 insertions(+), 10 deletions(-) diff --git a/terminal21-ui-std-exports/src/main/scala/org/terminal21/ui/std/ServerJson.scala b/terminal21-ui-std-exports/src/main/scala/org/terminal21/ui/std/ServerJson.scala index 5ff4371b..ae421522 100644 --- a/terminal21-ui-std-exports/src/main/scala/org/terminal21/ui/std/ServerJson.scala +++ b/terminal21-ui-std-exports/src/main/scala/org/terminal21/ui/std/ServerJson.scala @@ -7,12 +7,21 @@ case class ServerJson( elements: Map[String, Json], keyTree: Map[String, Seq[String]] ): + private def allChildren(k: String): Seq[String] = + val ch = keyTree(k) + ch ++ ch.flatMap(allChildren) + def include(j: ServerJson): ServerJson = - ServerJson( + val allCurrentChildren = j.rootKeys.flatMap(allChildren) +// println(s"Removing : ${allCurrentChildren.mkString(",")}") +// println(s"j Elements : ${j.elements.keys.toList.sorted.mkString(", ")}") + val sj = ServerJson( rootKeys, - elements ++ j.elements, - keyTree ++ j.keyTree + (elements -- allCurrentChildren) ++ j.elements, + (keyTree -- allCurrentChildren) ++ j.keyTree ) +// println(s"New Elements : ${sj.elements.keys.toList.sorted.mkString(", ")}") + sj object ServerJson: val Empty = ServerJson(Nil, Map.empty, Map.empty) diff --git a/terminal21-ui-std-exports/src/test/scala/org/terminal21/ui/std/ServerJsonTest.scala b/terminal21-ui-std-exports/src/test/scala/org/terminal21/ui/std/ServerJsonTest.scala index 4a2f7dbf..bd5eebf7 100644 --- a/terminal21-ui-std-exports/src/test/scala/org/terminal21/ui/std/ServerJsonTest.scala +++ b/terminal21-ui-std-exports/src/test/scala/org/terminal21/ui/std/ServerJsonTest.scala @@ -6,12 +6,24 @@ import org.scalatest.matchers.should.Matchers.* class ServerJsonTest extends AnyFunSuiteLike: test("include"): - val j1 = ServerJson(Seq("k1"), Map("k1" -> Json.fromInt(1), "k2" -> Json.fromInt(2)), Map("k1" -> Seq("k2", "k3"))) - val j2 = ServerJson(Nil, Map("k2" -> Json.fromInt(3)), Map("k2" -> Seq("k4"))) + val j1 = ServerJson(Seq("k1"), Map("k1" -> Json.fromInt(1), "k2" -> Json.fromInt(2), "k3" -> Json.fromInt(3)), Map("k1" -> Seq("k2", "k3"), "k2" -> Nil)) + val j2 = ServerJson(Seq("k2"), Map("k2" -> Json.fromInt(3)), Map("k2" -> Seq("k4"))) j1.include(j2) should be( ServerJson( Seq("k1"), - Map("k1" -> Json.fromInt(1), "k2" -> Json.fromInt(3)), + Map("k1" -> Json.fromInt(1), "k2" -> Json.fromInt(3), "k3" -> Json.fromInt(3)), Map("k1" -> Seq("k2", "k3"), "k2" -> Seq("k4")) ) ) + + test("include drops unused keys"): + val j1 = ServerJson( + Seq("root1"), + Map("root1" -> Json.fromInt(1), "k2" -> Json.fromInt(2), "k2-c1" -> Json.fromInt(21), "k2-c1-c1" -> Json.fromInt(211)), + Map("k1" -> Seq("k2"), "k2" -> Seq("k2-c1"), "k2-c1" -> Seq("k2-c1-c1"), "k2-c1-c1" -> Nil) + ) + val j2 = ServerJson(Seq("k2"), Map("k2" -> Json.fromInt(3)), Map("k2" -> Nil)) + val r = j1.include(j2) + r.rootKeys should be(Seq("root1")) + r.keyTree should be(Map("k1" -> Seq("k2"), "k2" -> Nil)) + r.elements should be(Map("root1" -> Json.fromInt(1), "k2" -> Json.fromInt(3))) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index d4018ade..c3b7cba8 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -1,6 +1,5 @@ package org.terminal21.client -import io.circe.* import org.terminal21.client.components.UiElement.HasChildren import org.terminal21.client.components.{UiComponent, UiElement} import org.terminal21.client.json.UiElementEncoding diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 7a8de601..9d5774ae 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -69,8 +69,8 @@ class Controller[M]( case _ => handled case _ => handled - private def invokeEventHandlers(handled: HandledEvent[M], event: CommandEvent): HandledEvent[M] = - eventHandlers.foldLeft(handled.copy(renderChanges = Nil, timedRenderChanges = Nil)): (h, f) => + private def invokeEventHandlers(initHandled: HandledEvent[M], event: CommandEvent): HandledEvent[M] = + eventHandlers.foldLeft(initHandled.copy(renderChanges = Nil, timedRenderChanges = Nil)): (h, f) => event match case OnClick(key) => val e = ControllerClickEvent(h.componentsByKey(key), h) @@ -82,7 +82,7 @@ class Controller[M]( case _: OnChangeBooleanEventHandler.CanHandleOnChangeEvent => ControllerChangeBooleanEvent(receivedBy, h, value.toBoolean) if f.isDefinedAt(e) then f(e) else h case ce: ClientEvent => - val e = ControllerClientEvent(handled, ce) + val e = ControllerClientEvent(h, ce) if f.isDefinedAt(e) then f(e) else h case x => throw new IllegalStateException(s"Unexpected state $x") From e715603682e8fc81a353ffb8080c24368143f2ad Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Sun, 25 Feb 2024 16:36:08 +0000 Subject: [PATCH 160/313] - --- .../terminal21/client/ConnectedSession.scala | 12 +- .../client/components/EventHandler.scala | 3 - .../client/components/UiElement.scala | 16 +- .../components/chakra/ChakraElement.scala | 137 +++++++++++------- .../client/components/std/StdElement.scala | 3 +- 5 files changed, 101 insertions(+), 70 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index c3b7cba8..6ef9d9ea 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -90,9 +90,9 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se ( el.key, el match - case e: UiComponent => encoding.uiElementEncoder(e).deepDropNullValues - case e: HasChildren[_] => encoding.uiElementEncoder(e.noChildren).deepDropNullValues - case e => encoding.uiElementEncoder(e).deepDropNullValues + case e: UiComponent => encoding.uiElementEncoder(e).deepDropNullValues + case e: HasChildren => encoding.uiElementEncoder(e.noChildren).deepDropNullValues + case e => encoding.uiElementEncoder(e).deepDropNullValues ) .toMap, flat @@ -100,9 +100,9 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se ( e.key, e match - case e: UiComponent => e.rendered.map(_.key) - case e: HasChildren[_] => e.children.map(_.key) - case _ => Nil + case e: UiComponent => e.rendered.map(_.key) + case e: HasChildren => e.children.map(_.key) + case _ => Nil ) .toMap ) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala index 06ca2b8c..9c3aef3e 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala @@ -8,7 +8,6 @@ trait EventHandler object OnClickEventHandler: trait CanHandleOnClickEvent extends HasDataStore: this: UiElement => - type This <: UiElement def onClick[M](using model: Model[M])(h: OnClickEventHandlerFunction[M]): This = val handlers = dataStore.getOrElse(model.ClickKey, Nil) store(model.ClickKey, handlers :+ h) @@ -16,7 +15,6 @@ object OnClickEventHandler: object OnChangeEventHandler: trait CanHandleOnChangeEvent extends HasDataStore with HasEventHandler: this: UiElement => - type This <: UiElement def onChange[M](using model: Model[M])(h: OnChangeEventHandlerFunction[M]): This = val handlers = dataStore.getOrElse(model.ChangeKey, Nil) store(model.ChangeKey, handlers :+ h) @@ -24,7 +22,6 @@ object OnChangeEventHandler: object OnChangeBooleanEventHandler: trait CanHandleOnChangeEvent extends HasDataStore with HasEventHandler: this: UiElement => - type This <: UiElement def onChange[M](using model: Model[M])(h: OnChangeBooleanEventHandlerFunction[M]): This = val handlers = dataStore.getOrElse(model.ChangeBooleanKey, Nil) store(model.ChangeBooleanKey, handlers :+ h) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala index 794149b5..5ffe4d88 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala @@ -3,6 +3,8 @@ package org.terminal21.client.components import org.terminal21.collections.{TypedMap, TypedMapKey} trait UiElement extends AnyElement: + type This <: UiElement + def key: String /** @return @@ -11,16 +13,16 @@ trait UiElement extends AnyElement: def flat: Seq[UiElement] = Seq(this) object UiElement: - trait HasChildren[A <: UiElement]: - this: A => + trait HasChildren: + this: UiElement => def children: Seq[UiElement] - override def flat: Seq[UiElement] = Seq(this) ++ children.flatMap(_.flat) - def withChildren(cn: UiElement*): A - def noChildren: A = withChildren() - def addChildren(cn: UiElement*): A = withChildren(children ++ cn: _*) + override def flat: Seq[UiElement] = Seq(this) ++ children.flatMap(_.flat) + def withChildren(cn: UiElement*): This + def noChildren: This = withChildren() + def addChildren(cn: UiElement*): This = withChildren(children ++ cn: _*) trait HasEventHandler: - type This <: UiElement + this: UiElement => def defaultEventHandler: String => This trait HasStyle[A <: UiElement]: diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala index e53155d9..4151e43a 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala @@ -64,7 +64,8 @@ case class ButtonGroup( style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil ) extends ChakraElement[ButtonGroup] - with HasChildren[ButtonGroup]: + with HasChildren: + type This = ButtonGroup override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]): ButtonGroup = copy(style = v) def withKey(v: String) = copy(key = v) @@ -89,7 +90,8 @@ case class Box( as: Option[String] = None, children: Seq[UiElement] = Nil ) extends ChakraElement[Box] - with HasChildren[Box]: + with HasChildren: + type This = Box override def withChildren(cn: UiElement*): Box = copy(children = cn) override def withStyle(v: Map[String, Any]): Box = copy(style = v) def withKey(v: String): Box = copy(key = v) @@ -109,7 +111,8 @@ case class HStack( style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil ) extends ChakraElement[HStack] - with HasChildren[HStack]: + with HasChildren: + type This = HStack override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -123,7 +126,8 @@ case class VStack( style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil ) extends ChakraElement[VStack] - with HasChildren[VStack]: + with HasChildren: + type This = VStack override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -139,7 +143,8 @@ case class SimpleGrid( children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty ) extends ChakraElement[SimpleGrid] - with HasChildren[SimpleGrid]: + with HasChildren: + type This = SimpleGrid override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -158,7 +163,7 @@ case class Editable( children: Seq[UiElement] = Nil, dataStore: TypedMap = TypedMap.empty ) extends ChakraElement[Editable] - with HasChildren[Editable] + with HasChildren with OnChangeEventHandler.CanHandleOnChangeEvent: type This = Editable override def defaultEventHandler = @@ -190,7 +195,8 @@ case class FormControl( style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil ) extends ChakraElement[FormControl] - with HasChildren[FormControl]: + with HasChildren: + type This = FormControl override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -204,7 +210,8 @@ case class FormLabel( style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil ) extends ChakraElement[FormLabel] - with HasChildren[FormLabel]: + with HasChildren: + type This = FormLabel override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -218,7 +225,8 @@ case class FormHelperText( style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil ) extends ChakraElement[FormHelperText] - with HasChildren[FormHelperText]: + with HasChildren: + type This = FormHelperText override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -256,7 +264,8 @@ case class InputGroup( style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil ) extends ChakraElement[InputGroup] - with HasChildren[InputGroup]: + with HasChildren: + type This = InputGroup override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -268,7 +277,8 @@ case class InputLeftAddon( style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil ) extends ChakraElement[InputLeftAddon] - with HasChildren[InputLeftAddon]: + with HasChildren: + type This = InputLeftAddon override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -280,7 +290,8 @@ case class InputRightAddon( style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil ) extends ChakraElement[InputRightAddon] - with HasChildren[InputRightAddon]: + with HasChildren: + type This = InputRightAddon override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -331,7 +342,7 @@ case class RadioGroup( children: Seq[UiElement] = Nil, dataStore: TypedMap = TypedMap.empty ) extends ChakraElement[RadioGroup] - with HasChildren[RadioGroup] + with HasChildren with OnChangeEventHandler.CanHandleOnChangeEvent: type This = RadioGroup override def defaultEventHandler = newValue => copy(valueReceived = Some(newValue)) @@ -352,7 +363,8 @@ case class Center( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[Center] - with HasChildren[Center]: + with HasChildren: + type This = Center override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -372,7 +384,8 @@ case class Circle( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[Circle] - with HasChildren[Circle]: + with HasChildren: + type This = Circle override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -392,7 +405,8 @@ case class Square( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[Square] - with HasChildren[Square]: + with HasChildren: + type This = Square override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1452,7 +1466,7 @@ case class Select( children: Seq[UiElement] = Nil, dataStore: TypedMap = TypedMap.empty ) extends ChakraElement[Select] - with HasChildren[Select] + with HasChildren with OnChangeEventHandler.CanHandleOnChangeEvent: type This = Select override def defaultEventHandler = newValue => copy(valueReceived = Some(newValue)) @@ -1482,7 +1496,8 @@ case class Option_( */ case class TableContainer(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty) extends ChakraElement[TableContainer] - with HasChildren[TableContainer]: + with HasChildren: + type This = TableContainer override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withRowStringData(data: Seq[Seq[String]]): TableContainer = withRowData(data.map(_.map(c => Text(text = c)))) @@ -1511,7 +1526,8 @@ case class Table( style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil ) extends ChakraElement[Table] - with HasChildren[Table]: + with HasChildren: + type This = Table override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1524,23 +1540,20 @@ case class TableCaption(key: String = Keys.nextKey, text: String = "", style: Ma def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) -case class Thead(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty) - extends ChakraElement[Thead] - with HasChildren[Thead]: +case class Thead(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty) extends ChakraElement[Thead] with HasChildren: + type This = Thead override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) -case class Tbody(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty) - extends ChakraElement[Tbody] - with HasChildren[Tbody]: +case class Tbody(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty) extends ChakraElement[Tbody] with HasChildren: + type This = Tbody override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) -case class Tfoot(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty) - extends ChakraElement[Tfoot] - with HasChildren[Tfoot]: +case class Tfoot(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty) extends ChakraElement[Tfoot] with HasChildren: + type This = Tfoot override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1550,7 +1563,8 @@ case class Tr( children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty ) extends ChakraElement[Tr] - with HasChildren[Tr]: + with HasChildren: + type This = Tr override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1562,7 +1576,8 @@ case class Th( children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty ) extends ChakraElement[Th] - with HasChildren[Th]: + with HasChildren: + type This = Th override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1576,7 +1591,8 @@ case class Td( style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil ) extends ChakraElement[Td] - with HasChildren[Td]: + with HasChildren: + type This = Td override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1585,9 +1601,8 @@ case class Td( /** https://chakra-ui.com/docs/components/menu/usage */ -case class Menu(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil) - extends ChakraElement[Menu] - with HasChildren[Menu]: +case class Menu(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil) extends ChakraElement[Menu] with HasChildren: + type This = Menu override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1600,7 +1615,8 @@ case class MenuButton( style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil ) extends ChakraElement[MenuButton] - with HasChildren[MenuButton]: + with HasChildren: + type This = MenuButton override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1610,7 +1626,8 @@ case class MenuButton( case class MenuList(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil) extends ChakraElement[MenuList] - with HasChildren[MenuList]: + with HasChildren: + type This = MenuList override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1622,7 +1639,7 @@ case class MenuItem( children: Seq[UiElement] = Nil, dataStore: TypedMap = TypedMap.empty ) extends ChakraElement[MenuItem] - with HasChildren[MenuItem] + with HasChildren with OnClickEventHandler.CanHandleOnClickEvent: type This = MenuItem override def withChildren(cn: UiElement*) = copy(children = cn) @@ -1644,7 +1661,8 @@ case class Badge( children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty ) extends ChakraElement[Badge] - with HasChildren[Badge]: + with HasChildren: + type This = Badge override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1706,7 +1724,8 @@ case class Code( style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil ) extends ChakraElement[Code] - with HasChildren[Code]: + with HasChildren: + type This = Code override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1719,7 +1738,8 @@ case class UnorderedList( style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil ) extends ChakraElement[UnorderedList] - with HasChildren[UnorderedList]: + with HasChildren: + type This = UnorderedList override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1731,7 +1751,8 @@ case class OrderedList( style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil ) extends ChakraElement[OrderedList] - with HasChildren[OrderedList]: + with HasChildren: + type This = OrderedList override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1743,7 +1764,8 @@ case class ListItem( style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil ) extends ChakraElement[ListItem] - with HasChildren[ListItem]: + with HasChildren: + type This = ListItem def withText(v: String) = copy(text = v) override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1755,7 +1777,8 @@ case class Alert( style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil ) extends ChakraElement[Alert] - with HasChildren[Alert]: + with HasChildren: + type This = Alert override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1815,7 +1838,8 @@ case class Tooltip( style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Seq(Text("use tooltip.withContent() to set this")) ) extends ChakraElement[Tooltip] - with HasChildren[Tooltip]: + with HasChildren: + type This = Tooltip override def withStyle(v: Map[String, Any]) = copy(style = v) def withContent(cn: UiElement) = withChildren(cn) def withKey(v: String) = copy(key = v) @@ -1840,7 +1864,8 @@ case class Tabs( style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil ) extends ChakraElement[Tabs] - with HasChildren[Tabs]: + with HasChildren: + type This = Tabs def withKey(v: String) = copy(key = v) override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1857,7 +1882,8 @@ case class TabList( style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil ) extends ChakraElement[TabList] - with HasChildren[TabList]: + with HasChildren: + type This = TabList def withKey(v: String) = copy(key = v) override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1874,7 +1900,8 @@ case class Tab( style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil ) extends ChakraElement[Tab] - with HasChildren[Tab]: + with HasChildren: + type This = Tab def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) override def withChildren(cn: UiElement*) = copy(children = cn) @@ -1894,7 +1921,8 @@ case class TabPanels( style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil ) extends ChakraElement[TabPanels] - with HasChildren[TabPanels]: + with HasChildren: + type This = TabPanels def withKey(v: String) = copy(key = v) override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1906,7 +1934,8 @@ case class TabPanel( style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil ) extends ChakraElement[TabPanel] - with HasChildren[TabPanel]: + with HasChildren: + type This = TabPanel def withKey(v: String) = copy(key = v) override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1923,7 +1952,8 @@ case class Breadcrumb( style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil ) extends ChakraElement[Breadcrumb] - with HasChildren[Breadcrumb]: + with HasChildren: + type This = Breadcrumb def withKey(v: String) = copy(key = v) override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1941,7 +1971,8 @@ case class BreadcrumbItem( style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil ) extends ChakraElement[BreadcrumbItem] - with HasChildren[BreadcrumbItem]: + with HasChildren: + type This = BreadcrumbItem def withKey(v: String) = copy(key = v) override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1957,7 +1988,7 @@ case class BreadcrumbLink( children: Seq[UiElement] = Nil, dataStore: TypedMap = TypedMap.empty ) extends ChakraElement[BreadcrumbLink] - with HasChildren[BreadcrumbLink] + with HasChildren with OnClickEventHandler.CanHandleOnClickEvent: type This = BreadcrumbLink def withKey(v: String) = copy(key = v) @@ -1977,7 +2008,7 @@ case class Link( children: Seq[UiElement] = Nil, dataStore: TypedMap = TypedMap.empty ) extends ChakraElement[Link] - with HasChildren[Link] + with HasChildren with OnClickEventHandler.CanHandleOnClickEvent: type This = Link def withKey(v: String) = copy(key = v) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala index 11fb311d..4581de5f 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala @@ -59,7 +59,8 @@ case class Paragraph( style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil ) extends StdElement[Paragraph] - with HasChildren[Paragraph]: + with HasChildren: + type This = Paragraph override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) From b6ab6b24d431e46021ae482e201e58d7b896f889 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Sun, 25 Feb 2024 16:50:24 +0000 Subject: [PATCH 161/313] - --- .../client/components/mathjax/MathJax.scala | 3 +- .../client/components/nivo/NivoElement.scala | 10 ++- .../client/components/UiElement.scala | 8 +- .../components/chakra/ChakraElement.scala | 76 ++++++++++++++++++- .../components/chakra/QuickFormControl.scala | 3 +- .../client/components/chakra/QuickTable.scala | 3 +- .../client/components/chakra/QuickTabs.scala | 3 +- .../client/components/std/StdElement.scala | 14 +++- 8 files changed, 102 insertions(+), 18 deletions(-) diff --git a/terminal21-mathjax/src/main/scala/org/terminal21/client/components/mathjax/MathJax.scala b/terminal21-mathjax/src/main/scala/org/terminal21/client/components/mathjax/MathJax.scala index 4deef557..88d12da5 100644 --- a/terminal21-mathjax/src/main/scala/org/terminal21/client/components/mathjax/MathJax.scala +++ b/terminal21-mathjax/src/main/scala/org/terminal21/client/components/mathjax/MathJax.scala @@ -13,7 +13,8 @@ case class MathJax( expression: String = """fill in the expression as per https://asciimath.org/""", style: Map[String, Any] = Map.empty // Note: some of the styles are ignored by mathjax lib ) extends MathJaxElement - with HasStyle[MathJax]: + with HasStyle: + type This = MathJax override def withStyle(v: Map[String, Any]): MathJax = copy(style = v) def withKey(k: String) = copy(key = k) def withExpression(e: String) = copy(expression = e) diff --git a/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/NivoElement.scala b/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/NivoElement.scala index 8816f500..ed91a09d 100644 --- a/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/NivoElement.scala +++ b/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/NivoElement.scala @@ -3,8 +3,8 @@ package org.terminal21.client.components.nivo import org.terminal21.client.components.UiElement.HasStyle import org.terminal21.client.components.{Keys, UiElement} -sealed trait NEJson extends UiElement -sealed trait NivoElement[A <: UiElement] extends NEJson with HasStyle[A] +sealed trait NEJson extends UiElement +sealed trait NivoElement extends NEJson with HasStyle /** https://nivo.rocks/line/ */ @@ -29,7 +29,8 @@ case class ResponsiveLine( pointLabelYOffset: Int = -12, useMesh: Boolean = true, legends: Seq[Legend] = Nil -) extends NivoElement[ResponsiveLine]: +) extends NivoElement: + type This = ResponsiveLine override def withStyle(v: Map[String, Any]): ResponsiveLine = copy(style = v) def withKey(v: String) = copy(key = v) def withData(data: Seq[Serie]) = copy(data = data) @@ -57,7 +58,8 @@ case class ResponsiveBar( axisLeft: Option[Axis] = Some(Axis(legend = "x", legendOffset = -40)), legends: Seq[Legend] = Nil, ariaLabel: String = "Chart Label" -) extends NivoElement[ResponsiveBar]: +) extends NivoElement: + type This = ResponsiveBar override def withStyle(v: Map[String, Any]): ResponsiveBar = copy(style = v) def withKey(v: String) = copy(key = v) def withData(data: Seq[Seq[BarDatum]]) = copy(data = data) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala index 5ffe4d88..7e6beea2 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala @@ -25,14 +25,14 @@ object UiElement: this: UiElement => def defaultEventHandler: String => This - trait HasStyle[A <: UiElement]: + trait HasStyle: + this: UiElement => def style: Map[String, Any] - def withStyle(v: Map[String, Any]): A - def withStyle(vs: (String, Any)*): A = withStyle(vs.toMap) + def withStyle(v: Map[String, Any]): This + def withStyle(vs: (String, Any)*): This = withStyle(vs.toMap) trait HasDataStore: this: UiElement => - type This <: UiElement def dataStore: TypedMap def withDataStore(ds: TypedMap): This def store[V](key: TypedMapKey[V], value: V): This = withDataStore(dataStore + (key -> value)) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala index 4151e43a..8c1f7936 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala @@ -1,8 +1,7 @@ package org.terminal21.client.components.chakra -import org.terminal21.client.ConnectedSession -import org.terminal21.client.components.UiElement.{HasChildren, HasEventHandler, HasStyle} import org.terminal21.client.components.* +import org.terminal21.client.components.UiElement.{HasChildren, HasStyle} import org.terminal21.collections.TypedMap sealed trait CEJson extends UiElement @@ -11,7 +10,7 @@ sealed trait CEJson extends UiElement * https://github.com/kostaskougios/terminal21-restapi/blob/main/examples/src/main/scala/tests/ChakraComponents.scala and it's related scala files under * https://github.com/kostaskougios/terminal21-restapi/tree/main/examples/src/main/scala/tests/chakra */ -sealed trait ChakraElement[A <: ChakraElement[A]] extends CEJson with HasStyle[A] +sealed trait ChakraElement[A <: ChakraElement[A]] extends CEJson with HasStyle /** https://chakra-ui.com/docs/components/button */ @@ -176,14 +175,17 @@ case class Editable( override def withDataStore(ds: TypedMap): Editable = copy(dataStore = ds) case class EditablePreview(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty) extends ChakraElement[EditablePreview]: + type This = EditablePreview override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) case class EditableInput(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty) extends ChakraElement[EditableInput]: + type This = EditableInput override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) case class EditableTextarea(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty) extends ChakraElement[EditableTextarea]: + type This = EditableTextarea override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -328,6 +330,7 @@ case class Radio( colorScheme: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[Radio]: + type This = Radio override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withValue(v: String) = copy(value = v) @@ -426,6 +429,7 @@ case class AddIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[AddIcon]: + type This = AddIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -443,6 +447,7 @@ case class ArrowBackIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[ArrowBackIcon]: + type This = ArrowBackIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -460,6 +465,7 @@ case class ArrowDownIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[ArrowDownIcon]: + type This = ArrowDownIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -477,6 +483,7 @@ case class ArrowForwardIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[ArrowForwardIcon]: + type This = ArrowForwardIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -494,6 +501,7 @@ case class ArrowLeftIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[ArrowLeftIcon]: + type This = ArrowLeftIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -511,6 +519,7 @@ case class ArrowRightIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[ArrowRightIcon]: + type This = ArrowRightIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -528,6 +537,7 @@ case class ArrowUpIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[ArrowUpIcon]: + type This = ArrowUpIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -545,6 +555,7 @@ case class ArrowUpDownIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[ArrowUpDownIcon]: + type This = ArrowUpDownIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -562,6 +573,7 @@ case class AtSignIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[AtSignIcon]: + type This = AtSignIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -579,6 +591,7 @@ case class AttachmentIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[AttachmentIcon]: + type This = AttachmentIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -596,6 +609,7 @@ case class BellIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[BellIcon]: + type This = BellIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -613,6 +627,7 @@ case class CalendarIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[CalendarIcon]: + type This = CalendarIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -630,6 +645,7 @@ case class ChatIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[ChatIcon]: + type This = ChatIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -647,6 +663,7 @@ case class CheckIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[CheckIcon]: + type This = CheckIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -664,6 +681,7 @@ case class CheckCircleIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[CheckCircleIcon]: + type This = CheckCircleIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -681,6 +699,7 @@ case class ChevronDownIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[ChevronDownIcon]: + type This = ChevronDownIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -698,6 +717,7 @@ case class ChevronLeftIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[ChevronLeftIcon]: + type This = ChevronLeftIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -715,6 +735,7 @@ case class ChevronRightIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[ChevronRightIcon]: + type This = ChevronRightIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -732,6 +753,7 @@ case class ChevronUpIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[ChevronUpIcon]: + type This = ChevronUpIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -749,6 +771,7 @@ case class CloseIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[CloseIcon]: + type This = CloseIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -766,6 +789,7 @@ case class CopyIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[CopyIcon]: + type This = CopyIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -783,6 +807,7 @@ case class DeleteIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[DeleteIcon]: + type This = DeleteIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -800,6 +825,7 @@ case class DownloadIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[DownloadIcon]: + type This = DownloadIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -817,6 +843,7 @@ case class DragHandleIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[DragHandleIcon]: + type This = DragHandleIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -834,6 +861,7 @@ case class EditIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[EditIcon]: + type This = EditIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -851,6 +879,7 @@ case class EmailIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[EmailIcon]: + type This = EmailIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -869,6 +898,7 @@ case class ExternalLinkIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[ExternalLinkIcon]: + type This = ExternalLinkIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -887,6 +917,7 @@ case class HamburgerIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[HamburgerIcon]: + type This = HamburgerIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -904,6 +935,7 @@ case class InfoIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[InfoIcon]: + type This = InfoIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -921,6 +953,7 @@ case class InfoOutlineIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[InfoOutlineIcon]: + type This = InfoOutlineIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -938,6 +971,7 @@ case class LinkIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[LinkIcon]: + type This = LinkIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -955,6 +989,7 @@ case class LockIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[LockIcon]: + type This = LockIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -972,6 +1007,7 @@ case class MinusIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[MinusIcon]: + type This = MinusIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -989,6 +1025,7 @@ case class MoonIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[MoonIcon]: + type This = MoonIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -1006,6 +1043,7 @@ case class NotAllowedIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[NotAllowedIcon]: + type This = NotAllowedIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -1023,6 +1061,7 @@ case class PhoneIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[PhoneIcon]: + type This = PhoneIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -1040,6 +1079,7 @@ case class PlusSquareIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[PlusSquareIcon]: + type This = PlusSquareIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -1057,6 +1097,7 @@ case class QuestionIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[QuestionIcon]: + type This = QuestionIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -1074,6 +1115,7 @@ case class QuestionOutlineIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[QuestionOutlineIcon]: + type This = QuestionOutlineIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -1091,6 +1133,7 @@ case class RepeatIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[RepeatIcon]: + type This = RepeatIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -1108,6 +1151,7 @@ case class RepeatClockIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[RepeatClockIcon]: + type This = RepeatClockIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -1125,6 +1169,7 @@ case class SearchIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[SearchIcon]: + type This = SearchIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -1142,6 +1187,7 @@ case class Search2Icon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[Search2Icon]: + type This = Search2Icon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -1159,6 +1205,7 @@ case class SettingsIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[SettingsIcon]: + type This = SettingsIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -1176,6 +1223,7 @@ case class SmallAddIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[SmallAddIcon]: + type This = SmallAddIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -1193,6 +1241,7 @@ case class SmallCloseIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[SmallCloseIcon]: + type This = SmallCloseIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -1210,6 +1259,7 @@ case class SpinnerIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[SpinnerIcon]: + type This = SpinnerIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -1227,6 +1277,7 @@ case class StarIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[StarIcon]: + type This = StarIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -1244,6 +1295,7 @@ case class SunIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[SunIcon]: + type This = SunIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -1261,6 +1313,7 @@ case class TimeIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[TimeIcon]: + type This = TimeIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -1278,6 +1331,7 @@ case class TriangleDownIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[TriangleDownIcon]: + type This = TriangleDownIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -1295,6 +1349,7 @@ case class TriangleUpIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[TriangleUpIcon]: + type This = TriangleUpIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -1312,6 +1367,7 @@ case class UnlockIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[UnlockIcon]: + type This = UnlockIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -1329,6 +1385,7 @@ case class UpDownIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[UpDownIcon]: + type This = UpDownIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -1346,6 +1403,7 @@ case class ViewIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[ViewIcon]: + type This = ViewIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -1363,6 +1421,7 @@ case class ViewOffIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[ViewOffIcon]: + type This = ViewOffIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -1380,6 +1439,7 @@ case class WarningIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[WarningIcon]: + type This = WarningIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -1397,6 +1457,7 @@ case class WarningTwoIcon( color: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[WarningTwoIcon]: + type This = WarningTwoIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withW(v: Option[String]) = copy(w = v) @@ -1487,6 +1548,7 @@ case class Option_( text: String = "", style: Map[String, Any] = Map.empty ) extends ChakraElement[Option_]: + type This = Option_ override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withValue(v: String) = copy(value = v) @@ -1536,6 +1598,7 @@ case class Table( def withColorScheme(v: Option[String]) = copy(colorScheme = v) case class TableCaption(key: String = Keys.nextKey, text: String = "", style: Map[String, Any] = Map.empty) extends ChakraElement[TableCaption]: + type This = TableCaption override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) @@ -1649,6 +1712,7 @@ case class MenuItem( override def withDataStore(ds: TypedMap): MenuItem = copy(dataStore = ds) case class MenuDivider(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty) extends ChakraElement[MenuDivider]: + type This = MenuDivider override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1685,6 +1749,7 @@ case class Image( borderRadius: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[Image]: + type This = Image override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withSrc(v: String) = copy(src = v) @@ -1706,6 +1771,7 @@ case class Text( decoration: Option[String] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[Text]: + type This = Text override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) @@ -1788,6 +1854,7 @@ case class AlertIcon( key: String = Keys.nextKey, style: Map[String, Any] = Map.empty ) extends ChakraElement[AlertIcon]: + type This = AlertIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1796,6 +1863,7 @@ case class AlertTitle( text: String = "Alert!", style: Map[String, Any] = Map.empty ) extends ChakraElement[AlertTitle]: + type This = AlertTitle override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) @@ -1805,6 +1873,7 @@ case class AlertDescription( text: String = "Something happened!", style: Map[String, Any] = Map.empty ) extends ChakraElement[AlertDescription]: + type This = AlertDescription override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) @@ -1820,6 +1889,7 @@ case class Progress( isIndeterminate: Option[Boolean] = None, style: Map[String, Any] = Map.empty ) extends ChakraElement[Progress]: + type This = Progress override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withColorScheme(v: Option[String]) = copy(colorScheme = v) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala index cd912a5c..bf0eb09e 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala @@ -10,7 +10,8 @@ case class QuickFormControl( inputGroup: Seq[UiElement] = Nil, helperText: Option[String] = None ) extends UiComponent - with HasStyle[QuickFormControl]: + with HasStyle: + type This = QuickFormControl lazy val rendered: Seq[UiElement] = val ch: Seq[UiElement] = label.map(l => FormLabel(key = key + "-label", text = l)).toSeq ++ diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala index 76ff9b2a..e4d11748 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala @@ -13,7 +13,8 @@ case class QuickTable( headers: Seq[UiElement] = Nil, rows: Seq[Seq[UiElement]] = Nil ) extends UiComponent - with HasStyle[QuickTable]: + with HasStyle: + type This = QuickTable def withKey(v: String) = copy(key = v) def withVariant(v: String) = copy(variant = v) def withColorScheme(v: String) = copy(colorScheme = v) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala index cea409a9..fa835881 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala @@ -9,7 +9,8 @@ case class QuickTabs( tabs: Seq[String | Seq[UiElement]] = Nil, tabPanels: Seq[Seq[UiElement]] = Nil ) extends UiComponent - with HasStyle[QuickTabs]: + with HasStyle: + type This = QuickTabs def withTabs(tabs: String | Seq[UiElement]*): QuickTabs = copy(tabs = tabs) def withTabPanels(tabPanels: Seq[UiElement]*): QuickTabs = copy(tabPanels = tabPanels) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala index 4581de5f..afdb0c49 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala @@ -1,54 +1,62 @@ package org.terminal21.client.components.std import org.terminal21.client.components.OnChangeEventHandler.CanHandleOnChangeEvent -import org.terminal21.client.components.UiElement.{HasChildren, HasEventHandler, HasStyle} +import org.terminal21.client.components.UiElement.{HasChildren, HasStyle} import org.terminal21.client.components.{Keys, OnChangeEventHandler, UiElement} -import org.terminal21.client.ConnectedSession import org.terminal21.collections.TypedMap sealed trait StdEJson extends UiElement -sealed trait StdElement[A <: UiElement] extends StdEJson with HasStyle[A] +sealed trait StdElement[A <: UiElement] extends StdEJson with HasStyle case class Span(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement[Span]: + type This = Span override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) case class NewLine(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty) extends StdElement[NewLine]: + type This = NewLine override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) case class Em(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement[Em]: + type This = Em override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) case class Header1(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement[Header1]: + type This = Header1 override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) case class Header2(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement[Header2]: + type This = Header2 override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) case class Header3(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement[Header3]: + type This = Header3 override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) case class Header4(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement[Header4]: + type This = Header4 override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) case class Header5(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement[Header5]: + type This = Header5 override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) case class Header6(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement[Header6]: + type This = Header6 override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) From a9a4dbbf81766299bfc99a4c550207b8f9a7f3c4 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Sun, 25 Feb 2024 16:55:49 +0000 Subject: [PATCH 162/313] - --- .../components/chakra/ChakraElement.scala | 246 +++++++++--------- .../client/json/UiElementEncoding.scala | 2 +- 2 files changed, 123 insertions(+), 125 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala index 8c1f7936..8f4d2fd9 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala @@ -10,7 +10,7 @@ sealed trait CEJson extends UiElement * https://github.com/kostaskougios/terminal21-restapi/blob/main/examples/src/main/scala/tests/ChakraComponents.scala and it's related scala files under * https://github.com/kostaskougios/terminal21-restapi/tree/main/examples/src/main/scala/tests/chakra */ -sealed trait ChakraElement[A <: ChakraElement[A]] extends CEJson with HasStyle +sealed trait ChakraElement extends CEJson with HasStyle /** https://chakra-ui.com/docs/components/button */ @@ -29,7 +29,7 @@ case class Button( isAttached: Option[Boolean] = None, spacing: Option[String] = None, dataStore: TypedMap = TypedMap.empty -) extends ChakraElement[Button] +) extends ChakraElement with OnClickEventHandler.CanHandleOnClickEvent: type This = Button override def withStyle(v: Map[String, Any]): Button = copy(style = v) @@ -62,7 +62,7 @@ case class ButtonGroup( borderColor: Option[String] = None, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil -) extends ChakraElement[ButtonGroup] +) extends ChakraElement with HasChildren: type This = ButtonGroup override def withChildren(cn: UiElement*) = copy(children = cn) @@ -88,7 +88,7 @@ case class Box( style: Map[String, Any] = Map.empty, as: Option[String] = None, children: Seq[UiElement] = Nil -) extends ChakraElement[Box] +) extends ChakraElement with HasChildren: type This = Box override def withChildren(cn: UiElement*): Box = copy(children = cn) @@ -109,7 +109,7 @@ case class HStack( align: Option[String] = None, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil -) extends ChakraElement[HStack] +) extends ChakraElement with HasChildren: type This = HStack override def withChildren(cn: UiElement*) = copy(children = cn) @@ -124,7 +124,7 @@ case class VStack( align: Option[String] = None, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil -) extends ChakraElement[VStack] +) extends ChakraElement with HasChildren: type This = VStack override def withChildren(cn: UiElement*) = copy(children = cn) @@ -141,7 +141,7 @@ case class SimpleGrid( columns: Int = 2, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty -) extends ChakraElement[SimpleGrid] +) extends ChakraElement with HasChildren: type This = SimpleGrid override def withChildren(cn: UiElement*) = copy(children = cn) @@ -161,7 +161,7 @@ case class Editable( style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, dataStore: TypedMap = TypedMap.empty -) extends ChakraElement[Editable] +) extends ChakraElement with HasChildren with OnChangeEventHandler.CanHandleOnChangeEvent: type This = Editable @@ -174,17 +174,17 @@ case class Editable( def value = valueReceived.getOrElse(defaultValue) override def withDataStore(ds: TypedMap): Editable = copy(dataStore = ds) -case class EditablePreview(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty) extends ChakraElement[EditablePreview]: +case class EditablePreview(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty) extends ChakraElement: type This = EditablePreview override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) -case class EditableInput(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty) extends ChakraElement[EditableInput]: +case class EditableInput(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty) extends ChakraElement: type This = EditableInput override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) -case class EditableTextarea(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty) extends ChakraElement[EditableTextarea]: +case class EditableTextarea(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty) extends ChakraElement: type This = EditableTextarea override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -196,7 +196,7 @@ case class FormControl( as: String = "", style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil -) extends ChakraElement[FormControl] +) extends ChakraElement with HasChildren: type This = FormControl override def withChildren(cn: UiElement*) = copy(children = cn) @@ -211,7 +211,7 @@ case class FormLabel( text: String, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil -) extends ChakraElement[FormLabel] +) extends ChakraElement with HasChildren: type This = FormLabel override def withChildren(cn: UiElement*) = copy(children = cn) @@ -226,7 +226,7 @@ case class FormHelperText( text: String, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil -) extends ChakraElement[FormHelperText] +) extends ChakraElement with HasChildren: type This = FormHelperText override def withChildren(cn: UiElement*) = copy(children = cn) @@ -246,7 +246,7 @@ case class Input( valueReceived: Option[String] = None, // use value instead style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty -) extends ChakraElement[Input] +) extends ChakraElement with OnChangeEventHandler.CanHandleOnChangeEvent: type This = Input override def defaultEventHandler = newValue => copy(valueReceived = Some(newValue)) @@ -265,7 +265,7 @@ case class InputGroup( size: String = "md", style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil -) extends ChakraElement[InputGroup] +) extends ChakraElement with HasChildren: type This = InputGroup override def withChildren(cn: UiElement*) = copy(children = cn) @@ -278,7 +278,7 @@ case class InputLeftAddon( text: String = "", style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil -) extends ChakraElement[InputLeftAddon] +) extends ChakraElement with HasChildren: type This = InputLeftAddon override def withChildren(cn: UiElement*) = copy(children = cn) @@ -291,7 +291,7 @@ case class InputRightAddon( text: String = "", style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil -) extends ChakraElement[InputRightAddon] +) extends ChakraElement with HasChildren: type This = InputRightAddon override def withChildren(cn: UiElement*) = copy(children = cn) @@ -309,7 +309,7 @@ case class Checkbox( style: Map[String, Any] = Map.empty, checkedV: Option[Boolean] = None, dataStore: TypedMap = TypedMap.empty -) extends ChakraElement[Checkbox] +) extends ChakraElement with OnChangeBooleanEventHandler.CanHandleOnChangeEvent: type This = Checkbox def checked: Boolean = checkedV.getOrElse(defaultChecked) @@ -329,7 +329,7 @@ case class Radio( text: String = "", colorScheme: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[Radio]: +) extends ChakraElement: type This = Radio override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -344,7 +344,7 @@ case class RadioGroup( style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, dataStore: TypedMap = TypedMap.empty -) extends ChakraElement[RadioGroup] +) extends ChakraElement with HasChildren with OnChangeEventHandler.CanHandleOnChangeEvent: type This = RadioGroup @@ -365,7 +365,7 @@ case class Center( h: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[Center] +) extends ChakraElement with HasChildren: type This = Center override def withChildren(cn: UiElement*) = copy(children = cn) @@ -386,7 +386,7 @@ case class Circle( h: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[Circle] +) extends ChakraElement with HasChildren: type This = Circle override def withChildren(cn: UiElement*) = copy(children = cn) @@ -407,7 +407,7 @@ case class Square( h: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[Square] +) extends ChakraElement with HasChildren: type This = Square override def withChildren(cn: UiElement*) = copy(children = cn) @@ -428,7 +428,7 @@ case class AddIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[AddIcon]: +) extends ChakraElement: type This = AddIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -446,7 +446,7 @@ case class ArrowBackIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[ArrowBackIcon]: +) extends ChakraElement: type This = ArrowBackIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -464,7 +464,7 @@ case class ArrowDownIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[ArrowDownIcon]: +) extends ChakraElement: type This = ArrowDownIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -482,7 +482,7 @@ case class ArrowForwardIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[ArrowForwardIcon]: +) extends ChakraElement: type This = ArrowForwardIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -500,7 +500,7 @@ case class ArrowLeftIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[ArrowLeftIcon]: +) extends ChakraElement: type This = ArrowLeftIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -518,7 +518,7 @@ case class ArrowRightIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[ArrowRightIcon]: +) extends ChakraElement: type This = ArrowRightIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -536,7 +536,7 @@ case class ArrowUpIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[ArrowUpIcon]: +) extends ChakraElement: type This = ArrowUpIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -554,7 +554,7 @@ case class ArrowUpDownIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[ArrowUpDownIcon]: +) extends ChakraElement: type This = ArrowUpDownIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -572,7 +572,7 @@ case class AtSignIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[AtSignIcon]: +) extends ChakraElement: type This = AtSignIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -590,7 +590,7 @@ case class AttachmentIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[AttachmentIcon]: +) extends ChakraElement: type This = AttachmentIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -608,7 +608,7 @@ case class BellIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[BellIcon]: +) extends ChakraElement: type This = BellIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -626,7 +626,7 @@ case class CalendarIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[CalendarIcon]: +) extends ChakraElement: type This = CalendarIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -644,7 +644,7 @@ case class ChatIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[ChatIcon]: +) extends ChakraElement: type This = ChatIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -662,7 +662,7 @@ case class CheckIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[CheckIcon]: +) extends ChakraElement: type This = CheckIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -680,7 +680,7 @@ case class CheckCircleIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[CheckCircleIcon]: +) extends ChakraElement: type This = CheckCircleIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -698,7 +698,7 @@ case class ChevronDownIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[ChevronDownIcon]: +) extends ChakraElement: type This = ChevronDownIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -716,7 +716,7 @@ case class ChevronLeftIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[ChevronLeftIcon]: +) extends ChakraElement: type This = ChevronLeftIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -734,7 +734,7 @@ case class ChevronRightIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[ChevronRightIcon]: +) extends ChakraElement: type This = ChevronRightIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -752,7 +752,7 @@ case class ChevronUpIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[ChevronUpIcon]: +) extends ChakraElement: type This = ChevronUpIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -770,7 +770,7 @@ case class CloseIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[CloseIcon]: +) extends ChakraElement: type This = CloseIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -788,7 +788,7 @@ case class CopyIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[CopyIcon]: +) extends ChakraElement: type This = CopyIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -806,7 +806,7 @@ case class DeleteIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[DeleteIcon]: +) extends ChakraElement: type This = DeleteIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -824,7 +824,7 @@ case class DownloadIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[DownloadIcon]: +) extends ChakraElement: type This = DownloadIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -842,7 +842,7 @@ case class DragHandleIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[DragHandleIcon]: +) extends ChakraElement: type This = DragHandleIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -860,7 +860,7 @@ case class EditIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[EditIcon]: +) extends ChakraElement: type This = EditIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -878,7 +878,7 @@ case class EmailIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[EmailIcon]: +) extends ChakraElement: type This = EmailIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -897,7 +897,7 @@ case class ExternalLinkIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[ExternalLinkIcon]: +) extends ChakraElement: type This = ExternalLinkIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -916,7 +916,7 @@ case class HamburgerIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[HamburgerIcon]: +) extends ChakraElement: type This = HamburgerIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -934,7 +934,7 @@ case class InfoIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[InfoIcon]: +) extends ChakraElement: type This = InfoIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -952,7 +952,7 @@ case class InfoOutlineIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[InfoOutlineIcon]: +) extends ChakraElement: type This = InfoOutlineIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -970,7 +970,7 @@ case class LinkIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[LinkIcon]: +) extends ChakraElement: type This = LinkIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -988,7 +988,7 @@ case class LockIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[LockIcon]: +) extends ChakraElement: type This = LockIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1006,7 +1006,7 @@ case class MinusIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[MinusIcon]: +) extends ChakraElement: type This = MinusIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1024,7 +1024,7 @@ case class MoonIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[MoonIcon]: +) extends ChakraElement: type This = MoonIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1042,7 +1042,7 @@ case class NotAllowedIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[NotAllowedIcon]: +) extends ChakraElement: type This = NotAllowedIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1060,7 +1060,7 @@ case class PhoneIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[PhoneIcon]: +) extends ChakraElement: type This = PhoneIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1078,7 +1078,7 @@ case class PlusSquareIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[PlusSquareIcon]: +) extends ChakraElement: type This = PlusSquareIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1096,7 +1096,7 @@ case class QuestionIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[QuestionIcon]: +) extends ChakraElement: type This = QuestionIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1114,7 +1114,7 @@ case class QuestionOutlineIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[QuestionOutlineIcon]: +) extends ChakraElement: type This = QuestionOutlineIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1132,7 +1132,7 @@ case class RepeatIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[RepeatIcon]: +) extends ChakraElement: type This = RepeatIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1150,7 +1150,7 @@ case class RepeatClockIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[RepeatClockIcon]: +) extends ChakraElement: type This = RepeatClockIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1168,7 +1168,7 @@ case class SearchIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[SearchIcon]: +) extends ChakraElement: type This = SearchIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1186,7 +1186,7 @@ case class Search2Icon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[Search2Icon]: +) extends ChakraElement: type This = Search2Icon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1204,7 +1204,7 @@ case class SettingsIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[SettingsIcon]: +) extends ChakraElement: type This = SettingsIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1222,7 +1222,7 @@ case class SmallAddIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[SmallAddIcon]: +) extends ChakraElement: type This = SmallAddIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1240,7 +1240,7 @@ case class SmallCloseIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[SmallCloseIcon]: +) extends ChakraElement: type This = SmallCloseIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1258,7 +1258,7 @@ case class SpinnerIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[SpinnerIcon]: +) extends ChakraElement: type This = SpinnerIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1276,7 +1276,7 @@ case class StarIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[StarIcon]: +) extends ChakraElement: type This = StarIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1294,7 +1294,7 @@ case class SunIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[SunIcon]: +) extends ChakraElement: type This = SunIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1312,7 +1312,7 @@ case class TimeIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[TimeIcon]: +) extends ChakraElement: type This = TimeIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1330,7 +1330,7 @@ case class TriangleDownIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[TriangleDownIcon]: +) extends ChakraElement: type This = TriangleDownIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1348,7 +1348,7 @@ case class TriangleUpIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[TriangleUpIcon]: +) extends ChakraElement: type This = TriangleUpIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1366,7 +1366,7 @@ case class UnlockIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[UnlockIcon]: +) extends ChakraElement: type This = UnlockIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1384,7 +1384,7 @@ case class UpDownIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[UpDownIcon]: +) extends ChakraElement: type This = UpDownIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1402,7 +1402,7 @@ case class ViewIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[ViewIcon]: +) extends ChakraElement: type This = ViewIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1420,7 +1420,7 @@ case class ViewOffIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[ViewOffIcon]: +) extends ChakraElement: type This = ViewOffIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1438,7 +1438,7 @@ case class WarningIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[WarningIcon]: +) extends ChakraElement: type This = WarningIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1456,7 +1456,7 @@ case class WarningTwoIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[WarningTwoIcon]: +) extends ChakraElement: type This = WarningTwoIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1477,7 +1477,7 @@ case class Textarea( valueReceived: Option[String] = None, // use value instead style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty -) extends ChakraElement[Textarea] +) extends ChakraElement with OnChangeEventHandler.CanHandleOnChangeEvent: type This = Textarea override def defaultEventHandler = newValue => copy(valueReceived = Some(newValue)) @@ -1501,7 +1501,7 @@ case class Switch( style: Map[String, Any] = Map.empty, checkedV: Option[Boolean] = None, // use checked dataStore: TypedMap = TypedMap.empty -) extends ChakraElement[Switch] +) extends ChakraElement with OnChangeBooleanEventHandler.CanHandleOnChangeEvent: type This = Switch def checked: Boolean = checkedV.getOrElse(defaultChecked) @@ -1526,7 +1526,7 @@ case class Select( style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, dataStore: TypedMap = TypedMap.empty -) extends ChakraElement[Select] +) extends ChakraElement with HasChildren with OnChangeEventHandler.CanHandleOnChangeEvent: type This = Select @@ -1547,7 +1547,7 @@ case class Option_( value: String, text: String = "", style: Map[String, Any] = Map.empty -) extends ChakraElement[Option_]: +) extends ChakraElement: type This = Option_ override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1557,7 +1557,7 @@ case class Option_( /** https://chakra-ui.com/docs/components/table/usage */ case class TableContainer(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty) - extends ChakraElement[TableContainer] + extends ChakraElement with HasChildren: type This = TableContainer override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1587,7 +1587,7 @@ case class Table( colorScheme: Option[String] = None, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil -) extends ChakraElement[Table] +) extends ChakraElement with HasChildren: type This = Table override def withChildren(cn: UiElement*) = copy(children = cn) @@ -1597,25 +1597,25 @@ case class Table( def withSize(v: String) = copy(size = v) def withColorScheme(v: Option[String]) = copy(colorScheme = v) -case class TableCaption(key: String = Keys.nextKey, text: String = "", style: Map[String, Any] = Map.empty) extends ChakraElement[TableCaption]: +case class TableCaption(key: String = Keys.nextKey, text: String = "", style: Map[String, Any] = Map.empty) extends ChakraElement: type This = TableCaption override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) -case class Thead(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty) extends ChakraElement[Thead] with HasChildren: +case class Thead(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty) extends ChakraElement with HasChildren: type This = Thead override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) -case class Tbody(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty) extends ChakraElement[Tbody] with HasChildren: +case class Tbody(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty) extends ChakraElement with HasChildren: type This = Tbody override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) -case class Tfoot(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty) extends ChakraElement[Tfoot] with HasChildren: +case class Tfoot(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty) extends ChakraElement with HasChildren: type This = Tfoot override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1625,7 +1625,7 @@ case class Tr( key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty -) extends ChakraElement[Tr] +) extends ChakraElement with HasChildren: type This = Tr override def withChildren(cn: UiElement*) = copy(children = cn) @@ -1638,7 +1638,7 @@ case class Th( isNumeric: Boolean = false, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty -) extends ChakraElement[Th] +) extends ChakraElement with HasChildren: type This = Th override def withChildren(cn: UiElement*) = copy(children = cn) @@ -1653,7 +1653,7 @@ case class Td( isNumeric: Boolean = false, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil -) extends ChakraElement[Td] +) extends ChakraElement with HasChildren: type This = Td override def withChildren(cn: UiElement*) = copy(children = cn) @@ -1664,7 +1664,7 @@ case class Td( /** https://chakra-ui.com/docs/components/menu/usage */ -case class Menu(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil) extends ChakraElement[Menu] with HasChildren: +case class Menu(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil) extends ChakraElement with HasChildren: type This = Menu override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1677,7 +1677,7 @@ case class MenuButton( colorScheme: Option[String] = None, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil -) extends ChakraElement[MenuButton] +) extends ChakraElement with HasChildren: type This = MenuButton override def withChildren(cn: UiElement*) = copy(children = cn) @@ -1687,9 +1687,7 @@ case class MenuButton( def withSize(v: Option[String]) = copy(size = v) def withColorScheme(v: Option[String]) = copy(colorScheme = v) -case class MenuList(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil) - extends ChakraElement[MenuList] - with HasChildren: +case class MenuList(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil) extends ChakraElement with HasChildren: type This = MenuList override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1701,7 +1699,7 @@ case class MenuItem( text: String = "", children: Seq[UiElement] = Nil, dataStore: TypedMap = TypedMap.empty -) extends ChakraElement[MenuItem] +) extends ChakraElement with HasChildren with OnClickEventHandler.CanHandleOnClickEvent: type This = MenuItem @@ -1711,7 +1709,7 @@ case class MenuItem( def withText(v: String) = copy(text = v) override def withDataStore(ds: TypedMap): MenuItem = copy(dataStore = ds) -case class MenuDivider(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty) extends ChakraElement[MenuDivider]: +case class MenuDivider(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty) extends ChakraElement: type This = MenuDivider override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1724,7 +1722,7 @@ case class Badge( size: String = "md", children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty -) extends ChakraElement[Badge] +) extends ChakraElement with HasChildren: type This = Badge override def withChildren(cn: UiElement*) = copy(children = cn) @@ -1748,7 +1746,7 @@ case class Image( boxSize: Option[String] = None, borderRadius: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[Image]: +) extends ChakraElement: type This = Image override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1770,7 +1768,7 @@ case class Text( casing: Option[String] = None, decoration: Option[String] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[Text]: +) extends ChakraElement: type This = Text override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1789,7 +1787,7 @@ case class Code( colorScheme: Option[String] = None, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil -) extends ChakraElement[Code] +) extends ChakraElement with HasChildren: type This = Code override def withChildren(cn: UiElement*) = copy(children = cn) @@ -1803,7 +1801,7 @@ case class UnorderedList( spacing: Option[String] = None, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil -) extends ChakraElement[UnorderedList] +) extends ChakraElement with HasChildren: type This = UnorderedList override def withChildren(cn: UiElement*) = copy(children = cn) @@ -1816,7 +1814,7 @@ case class OrderedList( spacing: Option[String] = None, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil -) extends ChakraElement[OrderedList] +) extends ChakraElement with HasChildren: type This = OrderedList override def withChildren(cn: UiElement*) = copy(children = cn) @@ -1829,7 +1827,7 @@ case class ListItem( text: String = "", style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil -) extends ChakraElement[ListItem] +) extends ChakraElement with HasChildren: type This = ListItem def withText(v: String) = copy(text = v) @@ -1842,7 +1840,7 @@ case class Alert( status: String = "error", // error, success, warning, info style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil -) extends ChakraElement[Alert] +) extends ChakraElement with HasChildren: type This = Alert override def withChildren(cn: UiElement*) = copy(children = cn) @@ -1853,7 +1851,7 @@ case class Alert( case class AlertIcon( key: String = Keys.nextKey, style: Map[String, Any] = Map.empty -) extends ChakraElement[AlertIcon]: +) extends ChakraElement: type This = AlertIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1862,7 +1860,7 @@ case class AlertTitle( key: String = Keys.nextKey, text: String = "Alert!", style: Map[String, Any] = Map.empty -) extends ChakraElement[AlertTitle]: +) extends ChakraElement: type This = AlertTitle override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1872,7 +1870,7 @@ case class AlertDescription( key: String = Keys.nextKey, text: String = "Something happened!", style: Map[String, Any] = Map.empty -) extends ChakraElement[AlertDescription]: +) extends ChakraElement: type This = AlertDescription override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1888,7 +1886,7 @@ case class Progress( hasStripe: Option[Boolean] = None, isIndeterminate: Option[Boolean] = None, style: Map[String, Any] = Map.empty -) extends ChakraElement[Progress]: +) extends ChakraElement: type This = Progress override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1907,7 +1905,7 @@ case class Tooltip( fontSize: Option[String] = None, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Seq(Text("use tooltip.withContent() to set this")) -) extends ChakraElement[Tooltip] +) extends ChakraElement with HasChildren: type This = Tooltip override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1933,7 +1931,7 @@ case class Tabs( isFitted: Option[Boolean] = None, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil -) extends ChakraElement[Tabs] +) extends ChakraElement with HasChildren: type This = Tabs def withKey(v: String) = copy(key = v) @@ -1951,7 +1949,7 @@ case class TabList( key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil -) extends ChakraElement[TabList] +) extends ChakraElement with HasChildren: type This = TabList def withKey(v: String) = copy(key = v) @@ -1969,7 +1967,7 @@ case class Tab( _active: Option[Map[String, Any]] = None, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil -) extends ChakraElement[Tab] +) extends ChakraElement with HasChildren: type This = Tab def withKey(v: String) = copy(key = v) @@ -1990,7 +1988,7 @@ case class TabPanels( key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil -) extends ChakraElement[TabPanels] +) extends ChakraElement with HasChildren: type This = TabPanels def withKey(v: String) = copy(key = v) @@ -2003,7 +2001,7 @@ case class TabPanel( key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil -) extends ChakraElement[TabPanel] +) extends ChakraElement with HasChildren: type This = TabPanel def withKey(v: String) = copy(key = v) @@ -2021,7 +2019,7 @@ case class Breadcrumb( pt: Option[Int] = None, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil -) extends ChakraElement[Breadcrumb] +) extends ChakraElement with HasChildren: type This = Breadcrumb def withKey(v: String) = copy(key = v) @@ -2040,7 +2038,7 @@ case class BreadcrumbItem( isCurrentPage: Option[Boolean] = None, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil -) extends ChakraElement[BreadcrumbItem] +) extends ChakraElement with HasChildren: type This = BreadcrumbItem def withKey(v: String) = copy(key = v) @@ -2057,7 +2055,7 @@ case class BreadcrumbLink( style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, dataStore: TypedMap = TypedMap.empty -) extends ChakraElement[BreadcrumbLink] +) extends ChakraElement with HasChildren with OnClickEventHandler.CanHandleOnClickEvent: type This = BreadcrumbLink @@ -2077,7 +2075,7 @@ case class Link( style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, dataStore: TypedMap = TypedMap.empty -) extends ChakraElement[Link] +) extends ChakraElement with HasChildren with OnClickEventHandler.CanHandleOnClickEvent: type This = Link diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/json/UiElementEncoding.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/json/UiElementEncoding.scala index 95d4519c..864dd040 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/json/UiElementEncoding.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/json/UiElementEncoding.scala @@ -38,7 +38,7 @@ object StdElementEncoding extends ComponentLib: case std: StdEJson => std.asJson.mapObject(o => o.add("type", "Std".asJson)) case c: CEJson => c.asJson.mapObject(o => o.add("type", "Chakra".asJson)) case c: UiComponent => - val b: ChakraElement[Box] = Box(key = c.key, text = "") + val b: ChakraElement = Box(key = c.key, text = "") b.asJson.mapObject(o => o.add("type", "Chakra".asJson)) case std: StdHttp => std.asJson.mapObject(o => o.add("type", "Std".asJson)) case fe: FrontEndElement => fe.asJson.mapObject(o => o.add("type", "FrontEnd".asJson)) From 3e9f6c5a21c3244d2002dbaddf3176a7e28e55fe Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Sun, 25 Feb 2024 16:56:48 +0000 Subject: [PATCH 163/313] - --- .../client/components/std/StdElement.scala | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala index afdb0c49..01b6bf54 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala @@ -5,57 +5,57 @@ import org.terminal21.client.components.UiElement.{HasChildren, HasStyle} import org.terminal21.client.components.{Keys, OnChangeEventHandler, UiElement} import org.terminal21.collections.TypedMap -sealed trait StdEJson extends UiElement -sealed trait StdElement[A <: UiElement] extends StdEJson with HasStyle +sealed trait StdEJson extends UiElement +sealed trait StdElement extends StdEJson with HasStyle -case class Span(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement[Span]: +case class Span(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement: type This = Span override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) -case class NewLine(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty) extends StdElement[NewLine]: +case class NewLine(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty) extends StdElement: type This = NewLine override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) -case class Em(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement[Em]: +case class Em(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement: type This = Em override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) -case class Header1(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement[Header1]: +case class Header1(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement: type This = Header1 override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) -case class Header2(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement[Header2]: +case class Header2(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement: type This = Header2 override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) -case class Header3(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement[Header3]: +case class Header3(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement: type This = Header3 override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) -case class Header4(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement[Header4]: +case class Header4(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement: type This = Header4 override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) -case class Header5(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement[Header5]: +case class Header5(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement: type This = Header5 override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) -case class Header6(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement[Header6]: +case class Header6(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement: type This = Header6 override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -66,7 +66,7 @@ case class Paragraph( text: String = "", style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil -) extends StdElement[Paragraph] +) extends StdElement with HasChildren: type This = Paragraph override def withChildren(cn: UiElement*) = copy(children = cn) @@ -81,7 +81,7 @@ case class Input( style: Map[String, Any] = Map.empty, valueReceived: Option[String] = None, // use value instead dataStore: TypedMap = TypedMap.empty -) extends StdElement[Input] +) extends StdElement with CanHandleOnChangeEvent: type This = Input override def defaultEventHandler = newValue => copy(valueReceived = Some(newValue)) From ac8b28fb24423d7027a429f731fc9f9ea2aa96b5 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Sun, 25 Feb 2024 17:04:57 +0000 Subject: [PATCH 164/313] - --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 65a3e7fb..87095574 100644 --- a/Readme.md +++ b/Readme.md @@ -172,7 +172,7 @@ Please use the [discussions](https://github.com/kostaskougios/terminal21-restapi - session builders refactoring for more flexible creation of sessions - QuickTabs - bug fix for old react state re-rendering on new session -- event iterators allows idiomatic handling of events +- event iterators allows idiomatic handling of events and overhaul of the event handling for easier testing ## Version 0.21 From 345b60ae30981220570754e949adce62323c7a6d Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Sun, 25 Feb 2024 18:44:12 +0000 Subject: [PATCH 165/313] - --- .../bundled/ServerStatusPageTest.scala | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala diff --git a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala new file mode 100644 index 00000000..bcf2dfd8 --- /dev/null +++ b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala @@ -0,0 +1,55 @@ +package org.terminal21.serverapp.bundled + +import org.scalatest.funsuite.AnyFunSuiteLike +import org.scalatestplus.mockito.MockitoSugar.mock +import org.terminal21.client.components.chakra.{Button, CheckIcon, NotAllowedIcon} +import org.terminal21.client.{ConnectedSession, ConnectedSessionMock} +import org.terminal21.model.CommonModelBuilders.session +import org.terminal21.model.{CommonModelBuilders, Session} +import org.terminal21.server.service.ServerSessionsService +import org.terminal21.serverapp.ServerSideSessions +import org.terminal21.client.given +import org.scalatest.matchers.should.Matchers.* + +class ServerStatusPageTest extends AnyFunSuiteLike: + class App: + given connectedSession: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock + val sessionsService = mock[ServerSessionsService] + val serverSideSessions = mock[ServerSideSessions] + val page = new ServerStatusPage(serverSideSessions, sessionsService) + + test("Close button for a session"): + new App: + page + .sessionsTable(Seq(session())) + .flat + .collectFirst: + case b: Button if b.text == "Close" => b + .isEmpty should be(false) + + test("View state button for a session"): + new App: + page + .sessionsTable(Seq(session())) + .flat + .collectFirst: + case b: Button if b.text == "View State" => b + .isEmpty should be(false) + + test("When session is open, a CheckIcon is displayed"): + new App: + page + .sessionsTable(Seq(session())) + .flat + .collectFirst: + case i: CheckIcon => i + .isEmpty should be(false) + + test("When session is closed, a NotAllowedIcon is displayed"): + new App: + page + .sessionsTable(Seq(session(isOpen = false))) + .flat + .collectFirst: + case i: NotAllowedIcon => i + .isEmpty should be(false) From 8139c25846ea5806f4c045478ead922246bc9c47 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Sun, 25 Feb 2024 18:58:44 +0000 Subject: [PATCH 166/313] - --- .../serverapp/bundled/ServerStatusApp.scala | 4 +-- .../bundled/ServerStatusPageTest.scala | 28 +++++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index dc88657d..4ac1e891 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -38,10 +38,10 @@ class ServerStatusPage( private def toMb(v: Long) = s"${v / (1024 * 1024)} MB" private val xs = Some("2xs") - def controller(runtime: Runtime, sessions: Seq[Session]): Controller[Unit] = + def controller(runtime: Runtime, sessions: => Seq[Session]): Controller[Unit] = Controller(components(runtime, sessions)).onEvent: case ControllerClientEvent(handled, Ticker) => - handled.withRenderChanges(sessionsTable(sessionsService.allSessions)) + handled.withRenderChanges(sessionsTable(sessions)) def components(runtime: Runtime, sessions: Seq[Session]): Seq[UiElement] = Seq(jvmTable(runtime), sessionsTable(sessions)) diff --git a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala index bcf2dfd8..d9e53d87 100644 --- a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala +++ b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala @@ -2,10 +2,10 @@ package org.terminal21.serverapp.bundled import org.scalatest.funsuite.AnyFunSuiteLike import org.scalatestplus.mockito.MockitoSugar.mock -import org.terminal21.client.components.chakra.{Button, CheckIcon, NotAllowedIcon} +import org.terminal21.client.components.chakra.{Button, CheckIcon, NotAllowedIcon, Text} import org.terminal21.client.{ConnectedSession, ConnectedSessionMock} import org.terminal21.model.CommonModelBuilders.session -import org.terminal21.model.{CommonModelBuilders, Session} +import org.terminal21.model.{CommandEvent, CommonModelBuilders, Session} import org.terminal21.server.service.ServerSessionsService import org.terminal21.serverapp.ServerSideSessions import org.terminal21.client.given @@ -53,3 +53,27 @@ class ServerStatusPageTest extends AnyFunSuiteLike: .collectFirst: case i: NotAllowedIcon => i .isEmpty should be(false) + + test("sessions are rendered when Ticker event is fired"): + new App: + var times = 0 + def sessions = + times += 1 + times match + case 1 => Seq(session(id = "s1", name = "session 1")) // this is initially rendered + case 2 => Seq(session(id = "s2", name = "session 2")) // this is a change + case 3 => Seq(session(id = "s3", name = "session 3")) // this is also a change + val it = page.controller(Runtime.getRuntime, sessions).handledEventsIterator + connectedSession.fireEvents(page.Ticker, page.Ticker, CommandEvent.sessionClosed) + val handledEvents = it.toList + handledEvents.head.renderChanges should be(Nil) + handledEvents(1).renderChanges + .flatMap(_.flat) + .collectFirst: + case t: Text if t.text == "session 2" => t + .size should be(1) + handledEvents(2).renderChanges + .flatMap(_.flat) + .collectFirst: + case t: Text if t.text == "session 3" => t + .size should be(1) From 9d5cd7c4ee5f166964e54f7356f5936fe2233593 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Sun, 25 Feb 2024 20:58:48 +0000 Subject: [PATCH 167/313] - --- .../sparklib/CalculationsExtensions.scala | 53 +++---- .../calculations/SparkCalculation.scala | 108 ++++++------- .../StdUiSparkCalculationTest.scala | 5 +- .../sparklib/endtoend/SparkBasics.scala | 17 ++- .../org/terminal21/client/Controller.scala | 26 +++- .../client/components/StdUiCalculation.scala | 144 ++++++++++-------- .../terminal21/client/CalculationTest.scala | 74 ++++----- .../terminal21/client/ControllerTest.scala | 9 +- 8 files changed, 237 insertions(+), 199 deletions(-) diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/CalculationsExtensions.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/CalculationsExtensions.scala index df93c8d9..66d51672 100644 --- a/terminal21-spark/src/main/scala/org/terminal21/sparklib/CalculationsExtensions.scala +++ b/terminal21-spark/src/main/scala/org/terminal21/sparklib/CalculationsExtensions.scala @@ -1,26 +1,27 @@ -//package org.terminal21.sparklib -// -//import functions.fibers.FiberExecutor -//import org.apache.spark.sql.SparkSession -//import org.terminal21.client.ConnectedSession -//import org.terminal21.client.components.UiElement.HasStyle -//import org.terminal21.client.components.{Keys, UiElement} -//import org.terminal21.sparklib.calculations.{ReadWriter, StdUiSparkCalculation} -// -//extension [OUT: ReadWriter](ds: OUT) -// def visualize(name: String, dataUi: UiElement with HasStyle[_])( -// toUi: OUT => UiElement & HasStyle[_] -// )(using -// session: ConnectedSession, -// executor: FiberExecutor, -// spark: SparkSession -// ) = -// val ui = new StdUiSparkCalculation[OUT](Keys.nextKey, name, dataUi): -// override protected def whenResultsReady(results: OUT): Unit = -// try updateUi(toUi(results)) -// catch case t: Throwable => t.printStackTrace() -// super.whenResultsReady(results) -// override def nonCachedCalculation: OUT = ds -// -// ui.run() -// ui +package org.terminal21.sparklib + +import functions.fibers.FiberExecutor +import org.apache.spark.sql.SparkSession +import org.terminal21.client.{ConnectedSession, Model} +import org.terminal21.client.components.UiElement.HasStyle +import org.terminal21.client.components.{Keys, UiElement} +import org.terminal21.sparklib.calculations.{ReadWriter, StdUiSparkCalculation} + +extension [OUT: ReadWriter](ds: OUT) + def visualize(name: String, dataUi: UiElement with HasStyle)( + toUi: OUT => UiElement & HasStyle + )(using + ConnectedSession, + Model[_], + FiberExecutor, + SparkSession + ) = + val ui = new StdUiSparkCalculation[OUT](Keys.nextKey, name, dataUi): + override protected def whenResultsReady(results: OUT): Unit = + try updateUi(toUi(results)) + catch case t: Throwable => t.printStackTrace() + super.whenResultsReady(results) + override def nonCachedCalculation: OUT = ds + + ui.run() + ui diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala index 3b2e664b..f456a6f5 100644 --- a/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala +++ b/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala @@ -1,54 +1,54 @@ -//package org.terminal21.sparklib.calculations -// -//import functions.fibers.FiberExecutor -//import org.apache.commons.io.FileUtils -//import org.apache.spark.sql.SparkSession -//import org.terminal21.client.ConnectedSession -//import org.terminal21.client.components.UiElement.HasStyle -//import org.terminal21.client.components.{CachedCalculation, StdUiCalculation, UiComponent, UiElement} -//import org.terminal21.sparklib.util.Environment -// -//import java.io.File -// -///** A UI component that takes a spark calculation (i.e. a spark query) that results in a Dataset. It caches the results by storing them as parquet into the tmp -// * folder/spark-calculations/$name. Next time the calculation runs it reads the cache if available. A button should allow the user to clear the cache and rerun -// * the spark calculations in case the data changed. -// * -// * Because the cache is stored in the disk, it is available even if the jvm running the code restarts. This allows the user to run and rerun their code without -// * having to rerun the spark calculation. -// * -// * Subclass this to create your own UI for a spark calculation, see StdUiSparkCalculation below. -// */ -//trait SparkCalculation[OUT: ReadWriter](name: String)(using executor: FiberExecutor, spark: SparkSession) extends CachedCalculation[OUT] with UiComponent: -// private val rw = implicitly[ReadWriter[OUT]] -// private val rootFolder = s"${Environment.tmpDirectory}/spark-calculations" -// private val targetDir = s"$rootFolder/$name" -// -// def isCached: Boolean = new File(targetDir).exists() -// def cachePath: String = targetDir -// -// private def cache[A](reader: => A, writer: => A): A = -// if isCached then reader -// else writer -// -// override def invalidateCache(): Unit = -// FileUtils.deleteDirectory(new File(targetDir)) -// -// private def calculateOnce(f: => OUT): OUT = -// cache( -// rw.read(spark, targetDir), { -// val ds = f -// rw.write(targetDir, ds) -// ds -// } -// ) -// -// override protected def calculation(): OUT = calculateOnce(nonCachedCalculation) -// -//abstract class StdUiSparkCalculation[OUT: ReadWriter]( -// val key: String, -// name: String, -// dataUi: UiElement with HasStyle[_] -//)(using session: ConnectedSession, executor: FiberExecutor, spark: SparkSession) -// extends SparkCalculation[OUT](name) -// with StdUiCalculation[OUT](name, dataUi) +package org.terminal21.sparklib.calculations + +import functions.fibers.FiberExecutor +import org.apache.commons.io.FileUtils +import org.apache.spark.sql.SparkSession +import org.terminal21.client.{ConnectedSession, Model} +import org.terminal21.client.components.UiElement.HasStyle +import org.terminal21.client.components.{CachedCalculation, StdUiCalculation, UiComponent, UiElement} +import org.terminal21.sparklib.util.Environment + +import java.io.File + +/** A UI component that takes a spark calculation (i.e. a spark query) that results in a Dataset. It caches the results by storing them as parquet into the tmp + * folder/spark-calculations/$name. Next time the calculation runs it reads the cache if available. A button should allow the user to clear the cache and rerun + * the spark calculations in case the data changed. + * + * Because the cache is stored in the disk, it is available even if the jvm running the code restarts. This allows the user to run and rerun their code without + * having to rerun the spark calculation. + * + * Subclass this to create your own UI for a spark calculation, see StdUiSparkCalculation below. + */ +trait SparkCalculation[OUT: ReadWriter](name: String)(using executor: FiberExecutor, spark: SparkSession) extends CachedCalculation[OUT] with UiComponent: + private val rw = implicitly[ReadWriter[OUT]] + private val rootFolder = s"${Environment.tmpDirectory}/spark-calculations" + private val targetDir = s"$rootFolder/$name" + + def isCached: Boolean = new File(targetDir).exists() + def cachePath: String = targetDir + + private def cache[A](reader: => A, writer: => A): A = + if isCached then reader + else writer + + override def invalidateCache(): Unit = + FileUtils.deleteDirectory(new File(targetDir)) + + private def calculateOnce(f: => OUT): OUT = + cache( + rw.read(spark, targetDir), { + val ds = f + rw.write(targetDir, ds) + ds + } + ) + + override protected def calculation(): OUT = calculateOnce(nonCachedCalculation) + +abstract class StdUiSparkCalculation[OUT: ReadWriter]( + val key: String, + name: String, + dataUi: UiElement with HasStyle +)(using ConnectedSession, Model[_], FiberExecutor, SparkSession) + extends SparkCalculation[OUT](name) + with StdUiCalculation[OUT](name, dataUi) diff --git a/terminal21-spark/src/test/scala/org/terminal21/sparklib/calculations/StdUiSparkCalculationTest.scala b/terminal21-spark/src/test/scala/org/terminal21/sparklib/calculations/StdUiSparkCalculationTest.scala index 13c7aadd..859d1c23 100644 --- a/terminal21-spark/src/test/scala/org/terminal21/sparklib/calculations/StdUiSparkCalculationTest.scala +++ b/terminal21-spark/src/test/scala/org/terminal21/sparklib/calculations/StdUiSparkCalculationTest.scala @@ -7,7 +7,7 @@ import org.scalatest.matchers.should.Matchers.* import org.scalatest.time.{Millis, Span} import org.terminal21.client.components.Keys import org.terminal21.client.components.chakra.* -import org.terminal21.client.{ConnectedSession, ConnectedSessionMock, given} +import org.terminal21.client.{ConnectedSession, ConnectedSessionMock, Model, given} import org.terminal21.sparklib.SparkSessions import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger} @@ -15,6 +15,7 @@ import scala.util.Using class StdUiSparkCalculationTest extends AnyFunSuiteLike with Eventually: given PatienceConfig = PatienceConfig(scaled(Span(3000, Millis))) + given Model[Unit] = Model.Standard.unitModel test("calculates the correct result"): Using.resource(SparkSessions.newSparkSession()): spark => @@ -88,7 +89,7 @@ class StdUiSparkCalculationTest extends AnyFunSuiteLike with Eventually: eventually: calc.calcCalledTimes.get() should be(2) -class TestingCalculation(using session: ConnectedSession, spark: SparkSession, enc: Encoder[Int]) +class TestingCalculation(using spark: SparkSession)(using ConnectedSession, Model[_], Encoder[Int]) extends StdUiSparkCalculation[Dataset[Int]](Keys.nextKey, "testing-calc", Box()): val calcCalledTimes = new AtomicInteger(0) invalidateCache() diff --git a/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala b/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala index d6cdc76e..7f039d14 100644 --- a/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala +++ b/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala @@ -20,6 +20,7 @@ import scala.util.Using .connect: session => given ConnectedSession = session given SparkSession = spark + given Model[Unit] = Model.Standard.unitModel import scala3encoders.given import spark.implicits.* @@ -65,14 +66,14 @@ import scala.util.Using val data = results.take(10).map(cf => Datum(StringUtils.substringBeforeLast(cf.name, ".scala"), cf.numOfLines)).toList chart.withData(Seq(Serie("Scala", data = data))) - Seq( - codeFilesCalculation, - sortedCalc, - sortedCalcAsDF, - sourceFileChart - ).render() - - session.waitTillUserClosesSession() + Controller( + Seq( + codeFilesCalculation, + sortedCalc, + sortedCalcAsDF, + sourceFileChart + ) + ).lastEventOption def sourceFiles()(using spark: SparkSession) = import scala3encoders.given diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 9d5774ae..20ff5001 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -1,5 +1,6 @@ package org.terminal21.client +import org.terminal21.client.Controller.defaultEventHandlers import org.terminal21.client.collections.EventIterator import org.terminal21.client.components.OnChangeEventHandler.CanHandleOnChangeEvent import org.terminal21.client.components.OnClickEventHandler.CanHandleOnClickEvent @@ -14,7 +15,7 @@ class Controller[M]( renderChanges: Seq[UiElement] => Unit, initialComponents: Seq[UiElement], initialModel: Model[M], - eventHandlers: Seq[PartialFunction[ControllerEvent[M], HandledEvent[M]]] + eventHandlers: Seq[PartialFunction[ControllerEvent[M], HandledEvent[M]]] = defaultEventHandlers[M] ): def render()(using session: ConnectedSession): this.type = session.render(initialComponents) @@ -137,10 +138,16 @@ class Controller[M]( ) object Controller: + private def renderChangesEventHandler[M]: PartialFunction[ControllerEvent[M], HandledEvent[M]] = + case ControllerClientEvent(handled, RenderChangesEvent(changes)) => + handled.withRenderChanges(changes*) + + private def defaultEventHandlers[M] = Seq(renderChangesEventHandler[M]) + def apply[M](initialModel: Model[M], components: Seq[UiElement])(using session: ConnectedSession): Controller[M] = - new Controller(session.eventIterator, session.fireEvent, session.renderChanges, components, initialModel, Nil) + new Controller(session.eventIterator, session.fireEvent, session.renderChanges, components, initialModel) def apply[M](components: Seq[UiElement])(using initialModel: Model[M], session: ConnectedSession): Controller[M] = - new Controller(session.eventIterator, session.fireEvent, session.renderChanges, components, initialModel, Nil) + new Controller(session.eventIterator, session.fireEvent, session.renderChanges, components, initialModel) sealed trait ControllerEvent[M]: def model: M = handled.model @@ -182,6 +189,13 @@ case class Model[M](value: M): object ChangeBooleanKey extends TypedMapKey[Seq[OnChangeBooleanEventHandlerFunction[M]]] object Model: - given unitModel: Model[Unit] = Model(()) - given booleanFalseModel: Model[Boolean] = Model(false) - given booleanTrueModel: Model[Boolean] = Model(true) + object Standard: + given unitModel: Model[Unit] = Model(()) + given booleanFalseModel: Model[Boolean] = Model(false) + given booleanTrueModel: Model[Boolean] = Model(true) + +/** Used to render changes outside the controller iteration + * @param changes + * the changes to be rendered + */ +case class RenderChangesEvent(changes: Seq[UiElement]) extends ClientEvent diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala index c2ab91b8..551316e0 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala @@ -1,65 +1,79 @@ -//package org.terminal21.client.components -// -//import functions.fibers.FiberExecutor -//import org.terminal21.client.ConnectedSession -//import org.terminal21.client.components.UiElement.HasStyle -//import org.terminal21.client.components.chakra.* -// -//import java.util.concurrent.atomic.{AtomicBoolean, AtomicReference} -// -///** Creates a standard UI for a calculation which may take time. While the calculation runs, the UI is grayed out, including the dataUi component. When the -// * calculation completes, it allows for updating the dataUi component. -// * @tparam OUT -// * the return value of the calculation. -// */ -//trait StdUiCalculation[OUT]( -// name: String, -// dataUi: UiElement with HasStyle[_] -//)(using session: ConnectedSession, executor: FiberExecutor) -// extends Calculation[OUT] -// with UiComponent: -// private val running = new AtomicBoolean(false) -// private val currentUi = new AtomicReference(dataUi) -// -// protected def updateUi(dataUi: UiElement & HasStyle[_]) = currentUi.set(dataUi) -// -// lazy val badge = Badge() -// lazy val recalc = Button(text = "Recalculate", size = Some("sm"), leftIcon = Some(RepeatIcon())).onClick: () => -// if running.compareAndSet(false, true) then -// try -// reCalculate() -// finally running.set(false) -// -// override lazy val rendered: Seq[UiElement] = -// val header = Box( -// bg = "green", -// p = 4, -// children = Seq( -// HStack(children = Seq(Text(text = name), badge, recalc)) -// ) -// ) -// Seq(header, dataUi) -// -// override def onError(t: Throwable): Unit = -// session.renderChanges( -// badge.withText(s"Error: ${t.getMessage}").withColorScheme(Some("red")), -// dataUi, -// recalc.withIsDisabled(None) -// ) -// super.onError(t) -// -// override protected def whenResultsNotReady(): Unit = -// session.renderChanges( -// badge.withText("Calculating").withColorScheme(Some("purple")), -// currentUi.get().withStyle(dataUi.style + ("filter" -> "grayscale(100%)")), -// recalc.withIsDisabled(Some(true)) -// ) -// super.whenResultsNotReady() -// -// override protected def whenResultsReady(results: OUT): Unit = -// val newDataUi = currentUi.get().withStyle(dataUi.style - "filter") -// session.renderChanges( -// badge.withText("Ready").withColorScheme(None), -// newDataUi, -// recalc.withIsDisabled(Some(false)) -// ) +package org.terminal21.client.components + +import functions.fibers.FiberExecutor +import org.terminal21.client.{ConnectedSession, Model, RenderChangesEvent} +import org.terminal21.client.components.UiElement.HasStyle +import org.terminal21.client.components.chakra.* + +import java.util.concurrent.atomic.{AtomicBoolean, AtomicReference} + +/** Creates a standard UI for a calculation which may take time. While the calculation runs, the UI is grayed out, including the dataUi component. When the + * calculation completes, it allows for updating the dataUi component. + * @tparam OUT + * the return value of the calculation. + */ +trait StdUiCalculation[OUT]( + name: String, + dataUi: UiElement with HasStyle +)(using session: ConnectedSession, model: Model[_], executor: FiberExecutor) + extends Calculation[OUT] + with UiComponent: + private val running = new AtomicBoolean(false) + private val currentUi = new AtomicReference(dataUi) + + protected def updateUi(dataUi: UiElement & HasStyle) = currentUi.set(dataUi) + + lazy val badge = Badge() + lazy val recalc = Button(text = "Recalculate", size = Some("sm"), leftIcon = Some(RepeatIcon())).onClick: event => + import event.* + if running.compareAndSet(false, true) then + try + reCalculate() + finally running.set(false) + handled + + override lazy val rendered: Seq[UiElement] = + val header = Box( + bg = "green", + p = 4, + children = Seq( + HStack(children = Seq(Text(text = name), badge, recalc)) + ) + ) + Seq(header, dataUi) + + override def onError(t: Throwable): Unit = + session.fireEvent( + RenderChangesEvent( + Seq( + badge.withText(s"Error: ${t.getMessage}").withColorScheme(Some("red")), + dataUi, + recalc.withIsDisabled(None) + ) + ) + ) + super.onError(t) + + override protected def whenResultsNotReady(): Unit = + session.fireEvent( + RenderChangesEvent( + Seq( + badge.withText("Calculating").withColorScheme(Some("purple")), + currentUi.get().withStyle(dataUi.style + ("filter" -> "grayscale(100%)")), + recalc.withIsDisabled(Some(true)) + ) + ) + ) + super.whenResultsNotReady() + + override protected def whenResultsReady(results: OUT): Unit = + val newDataUi = currentUi.get().withStyle(dataUi.style - "filter") + session.fireEvent( + RenderChangesEvent( + Seq( + badge.withText("Ready").withColorScheme(None), + newDataUi, + recalc.withIsDisabled(Some(false)) + ) + ) + ) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/CalculationTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/CalculationTest.scala index 81e38136..9fea22ac 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/CalculationTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/CalculationTest.scala @@ -1,37 +1,37 @@ -//package org.terminal21.client -// -//import functions.fibers.FiberExecutor -//import org.scalatest.funsuite.AnyFunSuiteLike -//import org.scalatest.matchers.should.Matchers.* -// -//import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger} -//import org.scalatest.concurrent.Eventually.* -//import org.terminal21.client.components.Calculation -// -//class CalculationTest extends AnyFunSuiteLike: -// given executor: FiberExecutor = FiberExecutor() -// def testCalc(i: Int) = i + 1 -// def testCalcString(i: Int): String = (i + 10).toString -// -// class Calc extends Calculation[Int]: -// val whenResultsNotReadyCalled = new AtomicBoolean(false) -// val whenResultsReadyValue = new AtomicInteger(-1) -// override protected def whenResultsNotReady(): Unit = whenResultsNotReadyCalled.set(true) -// override protected def whenResultsReady(results: Int): Unit = whenResultsReadyValue.set(results) -// override protected def calculation() = 2 -// -// test("calculates"): -// val calc = new Calc -// calc.run().get() should be(2) -// -// test("calls whenResultsNotReady"): -// val calc = new Calc -// calc.run() -// eventually: -// calc.whenResultsNotReadyCalled.get() should be(true) -// -// test("calls whenResultsReady"): -// val calc = new Calc -// calc.run() -// eventually: -// calc.whenResultsReadyValue.get() should be(2) +package org.terminal21.client + +import functions.fibers.FiberExecutor +import org.scalatest.funsuite.AnyFunSuiteLike +import org.scalatest.matchers.should.Matchers.* + +import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger} +import org.scalatest.concurrent.Eventually.* +import org.terminal21.client.components.Calculation + +class CalculationTest extends AnyFunSuiteLike: + given executor: FiberExecutor = FiberExecutor() + def testCalc(i: Int) = i + 1 + def testCalcString(i: Int): String = (i + 10).toString + + class Calc extends Calculation[Int]: + val whenResultsNotReadyCalled = new AtomicBoolean(false) + val whenResultsReadyValue = new AtomicInteger(-1) + override protected def whenResultsNotReady(): Unit = whenResultsNotReadyCalled.set(true) + override protected def whenResultsReady(results: Int): Unit = whenResultsReadyValue.set(results) + override protected def calculation() = 2 + + test("calculates"): + val calc = new Calc + calc.run().get() should be(2) + + test("calls whenResultsNotReady"): + val calc = new Calc + calc.run() + eventually: + calc.whenResultsNotReadyCalled.get() should be(true) + + test("calls whenResultsReady"): + val calc = new Calc + calc.run() + eventually: + calc.whenResultsReadyValue.get() should be(2) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index f2f1e3a0..12be6acb 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -26,7 +26,7 @@ class ControllerTest extends AnyFunSuiteLike: val it = seList.iterator events.foreach(e => seList.add(e)) seList.add(CommandEvent.sessionClosed) - new Controller(it, event => (), renderChanges, components, initialModel, Nil) + new Controller(it, _ => (), renderChanges, components, initialModel) test("onEvent is called"): val model = Model(0) @@ -252,3 +252,10 @@ class ControllerTest extends AnyFunSuiteLike: lazy val box: Box = Box().withChildren(b) newController(model, Seq(buttonClick, checkBoxChange), Seq(box)).eventsIterator.toList should be(List(0, 1, 2)) + + test("RenderChangesEvent renders changes"): + var rendered = Seq.empty[UiElement] + def renderer(s: Seq[UiElement]): Unit = rendered = s + + newController(Model(0), Seq(RenderChangesEvent(Seq(button.withText("changed")))), Seq(button), renderer).eventsIterator.toList should be(List(0, 0)) + rendered should be(Seq(button.withText("changed"))) From 4f191c7caa51e7be3e584de33d2290a8d34c0830 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 26 Feb 2024 12:17:28 +0000 Subject: [PATCH 168/313] - --- end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala | 2 +- end-to-end-tests/src/main/scala/tests/NivoComponents.scala | 2 +- .../src/main/scala/tests/StateSessionStateBug.scala | 2 +- end-to-end-tests/src/main/scala/tests/StdComponents.scala | 2 +- .../org/terminal21/serverapp/bundled/ServerStatusApp.scala | 2 +- .../scala/org/terminal21/serverapp/bundled/SettingsApp.scala | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala b/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala index 1c0c3b35..54bf44cb 100644 --- a/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala @@ -11,7 +11,7 @@ import org.terminal21.client.components.mathjax.* .andLibraries(MathJaxLib) .connect: session => given ConnectedSession = session - import Model.unitModel + import Model.Standard.unitModel val components = Seq( HStack().withChildren( diff --git a/end-to-end-tests/src/main/scala/tests/NivoComponents.scala b/end-to-end-tests/src/main/scala/tests/NivoComponents.scala index ecb18dc0..94cd0887 100644 --- a/end-to-end-tests/src/main/scala/tests/NivoComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/NivoComponents.scala @@ -11,6 +11,6 @@ import tests.nivo.{ResponsiveBarChart, ResponsiveLineChart} .andLibraries(NivoLib) .connect: session => given ConnectedSession = session - import Model.unitModel + import Model.Standard.unitModel val components = ResponsiveBarChart() ++ ResponsiveLineChart() Controller(components).eventsIterator.lastOption diff --git a/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala b/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala index b248b471..7893d914 100644 --- a/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala +++ b/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala @@ -12,7 +12,7 @@ import java.util.Date .withNewSession("state-session", "Stale Session") .connect: session => given ConnectedSession = session - import Model.unitModel + import Model.Standard.unitModel val date = new Date() val components = Seq( diff --git a/end-to-end-tests/src/main/scala/tests/StdComponents.scala b/end-to-end-tests/src/main/scala/tests/StdComponents.scala index c3be8210..ffee8ff4 100644 --- a/end-to-end-tests/src/main/scala/tests/StdComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/StdComponents.scala @@ -9,7 +9,7 @@ import org.terminal21.client.components.std.* .withNewSession("std-components", "Std Components") .connect: session => given ConnectedSession = session - import Model.unitModel + import Model.Standard.unitModel val output = Paragraph(text = "This will reflect what you type in the input") val cookieValue = Paragraph(text = "This will display the value of the cookie") diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 4ac1e891..cd18e313 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -24,7 +24,7 @@ class ServerStatusPage( serverSideSessions: ServerSideSessions, sessionsService: ServerSessionsService )(using appSession: ConnectedSession, fiberExecutor: FiberExecutor): - import Model.unitModel + import Model.Standard.unitModel case object Ticker extends ClientEvent diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala index 21ab327e..94f97377 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala @@ -19,7 +19,7 @@ class SettingsApp extends ServerSideApp: new SettingsPage().run() class SettingsPage(using session: ConnectedSession): - import Model.unitModel + import Model.Standard.unitModel val themeToggle = ThemeToggle() def run() = controller.render().eventsIterator.lastOption From f39c4e13e1369a37a8716d51895e8c6c6d135cd3 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 26 Feb 2024 12:29:30 +0000 Subject: [PATCH 169/313] - --- .../sparklib/calculations/StdUiSparkCalculationTest.scala | 7 +++++-- .../scala/org/terminal21/client/ConnectedSession.scala | 4 +++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/terminal21-spark/src/test/scala/org/terminal21/sparklib/calculations/StdUiSparkCalculationTest.scala b/terminal21-spark/src/test/scala/org/terminal21/sparklib/calculations/StdUiSparkCalculationTest.scala index 859d1c23..e363c9e0 100644 --- a/terminal21-spark/src/test/scala/org/terminal21/sparklib/calculations/StdUiSparkCalculationTest.scala +++ b/terminal21-spark/src/test/scala/org/terminal21/sparklib/calculations/StdUiSparkCalculationTest.scala @@ -7,7 +7,7 @@ import org.scalatest.matchers.should.Matchers.* import org.scalatest.time.{Millis, Span} import org.terminal21.client.components.Keys import org.terminal21.client.components.chakra.* -import org.terminal21.client.{ConnectedSession, ConnectedSessionMock, Model, given} +import org.terminal21.client.{ConnectedSession, ConnectedSessionMock, Controller, Model, given} import org.terminal21.sparklib.SparkSessions import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger} @@ -85,7 +85,10 @@ class StdUiSparkCalculationTest extends AnyFunSuiteLike with Eventually: given SparkSession = spark val calc = new TestingCalculation calc.run().get().collect().toList should be(List(2)) - session.click(calc.recalc) + val it = Controller(Seq(calc)).eventsIterator + session.fireClickEvent(calc.recalc) + session.fireSessionClosedEvent() + it.lastOption eventually: calc.calcCalledTimes.get() should be(2) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index 6ef9d9ea..fc0aa98f 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -52,7 +52,9 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se */ def isClosed: Boolean = exitLatch.getCount == 0 - def click(e: UiElement): Unit = fireEvent(OnClick(e.key)) + def fireClickEvent(e: UiElement): Unit = fireEvent(CommandEvent.onClick(e)) + def fireChangeEvent(e: UiElement, newValue: String): Unit = fireEvent(CommandEvent.onChange(e, newValue)) + def fireSessionClosedEvent(): Unit = fireEvent(CommandEvent.sessionClosed) def eventIterator: Iterator[CommandEvent] = events.iterator From 80c9e3fd948fb6539f6cfc97edb2e55c2392290f Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 26 Feb 2024 12:47:33 +0000 Subject: [PATCH 170/313] - --- .../src/main/scala/tests/LoginForm.scala | 4 +- .../sparklib/endtoend/SparkBasics.scala | 2 +- .../org/terminal21/ui/std/ServerJson.scala | 39 ++++++++++++++----- .../terminal21/client/ConnectedSession.scala | 1 + 4 files changed, 33 insertions(+), 13 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/LoginForm.scala b/end-to-end-tests/src/main/scala/tests/LoginForm.scala index b9e8cb41..6f4ad872 100644 --- a/end-to-end-tests/src/main/scala/tests/LoginForm.scala +++ b/end-to-end-tests/src/main/scala/tests/LoginForm.scala @@ -45,7 +45,7 @@ class LoginForm(using session: ConnectedSession): val errorMsgInvalidEmail = Paragraph(text = "Invalid Email", style = Map("color" -> "red")) def run(): Option[Login] = - controller.eventsIterator.lastOptionOrNoneIfSessionClosed + controller.render().eventsIterator.lastOptionOrNoneIfSessionClosed def components: Seq[UiElement] = Seq( @@ -91,7 +91,7 @@ class LoggedIn(login: Login)(using session: ConnectedSession): val passwordDetails = Text(text = s"password : ${login.pwd}") def run(): Option[Boolean] = - controller.eventsIterator.lastOption + controller.render().eventsIterator.lastOption def components = Seq( diff --git a/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala b/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala index 7f039d14..aa273043 100644 --- a/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala +++ b/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala @@ -73,7 +73,7 @@ import scala.util.Using sortedCalcAsDF, sourceFileChart ) - ).lastEventOption + ).render().lastEventOption def sourceFiles()(using spark: SparkSession) = import scala3encoders.given diff --git a/terminal21-ui-std-exports/src/main/scala/org/terminal21/ui/std/ServerJson.scala b/terminal21-ui-std-exports/src/main/scala/org/terminal21/ui/std/ServerJson.scala index ae421522..abdae823 100644 --- a/terminal21-ui-std-exports/src/main/scala/org/terminal21/ui/std/ServerJson.scala +++ b/terminal21-ui-std-exports/src/main/scala/org/terminal21/ui/std/ServerJson.scala @@ -1,6 +1,7 @@ package org.terminal21.ui.std import io.circe.Json +import org.slf4j.LoggerFactory case class ServerJson( rootKeys: Seq[String], @@ -12,16 +13,34 @@ case class ServerJson( ch ++ ch.flatMap(allChildren) def include(j: ServerJson): ServerJson = - val allCurrentChildren = j.rootKeys.flatMap(allChildren) -// println(s"Removing : ${allCurrentChildren.mkString(",")}") -// println(s"j Elements : ${j.elements.keys.toList.sorted.mkString(", ")}") - val sj = ServerJson( - rootKeys, - (elements -- allCurrentChildren) ++ j.elements, - (keyTree -- allCurrentChildren) ++ j.keyTree - ) -// println(s"New Elements : ${sj.elements.keys.toList.sorted.mkString(", ")}") - sj + try + val allCurrentChildren = j.rootKeys.flatMap(allChildren) + val sj = ServerJson( + rootKeys, + (elements -- allCurrentChildren) ++ j.elements, + (keyTree -- allCurrentChildren) ++ j.keyTree + ) + sj + catch + case t: Throwable => + LoggerFactory + .getLogger(getClass) + .error( + s""" + |Got an invalid ServerJson that caused an error. + |Before receiving: + |${toHumanReadableString} + |The received: + |${j.toHumanReadableString} + |""".stripMargin + ) + throw t + def toHumanReadableString: String = + s""" + |Root keys : ${rootKeys.mkString(", ")} + |Element keys : ${elements.keys.mkString(", ")} + |keyTree : ${keyTree.mkString(", ")} + |""".stripMargin object ServerJson: val Empty = ServerJson(Nil, Map.empty, Map.empty) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index fc0aa98f..3e062254 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -75,6 +75,7 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se case _ => def render(es: Seq[UiElement]): Unit = + clear() val j = toJson(es) sessionsService.setSessionJsonState(session, j) From 9d0523a8b59cd3500a8694caf6f776d6abb8b67d Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 26 Feb 2024 13:00:18 +0000 Subject: [PATCH 171/313] - --- .../scala/org/terminal21/sparklib/CalculationsExtensions.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/CalculationsExtensions.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/CalculationsExtensions.scala index 66d51672..3e12323c 100644 --- a/terminal21-spark/src/main/scala/org/terminal21/sparklib/CalculationsExtensions.scala +++ b/terminal21-spark/src/main/scala/org/terminal21/sparklib/CalculationsExtensions.scala @@ -8,7 +8,7 @@ import org.terminal21.client.components.{Keys, UiElement} import org.terminal21.sparklib.calculations.{ReadWriter, StdUiSparkCalculation} extension [OUT: ReadWriter](ds: OUT) - def visualize(name: String, dataUi: UiElement with HasStyle)( + def visualize(name: String, dataUi: UiElement & HasStyle)( toUi: OUT => UiElement & HasStyle )(using ConnectedSession, From d7d4c9bf38b4f5fac2e0bf74bfca1d2bc5d3a2ca Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 26 Feb 2024 14:48:58 +0000 Subject: [PATCH 172/313] - --- .../terminal21/client/ConnectedSession.scala | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index 3e062254..871cba33 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -11,12 +11,25 @@ import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.{CountDownLatch, TimeUnit} import scala.annotation.tailrec +/** A session connected to the terminal21 server. + * + * @param session + * the session + * @param encoding + * json encoder for UiElements + * @param serverUrl + * the url of the server + * @param sessionsService + * the service to talk to the server + * @param onCloseHandler + * gets notified when the user closes the session + */ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val serverUrl: String, sessionsService: SessionsService, onCloseHandler: () => Unit): @volatile private var events = SEList[CommandEvent]() def uiUrl: String = serverUrl + "/ui" - /** Clears all UI elements and event handlers. Renders a blank UI + /** Clears all UI elements and event handlers. */ def clear(): Unit = events.poisonPill() @@ -32,7 +45,8 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se private val leaveSessionOpen = new AtomicBoolean(false) - /** Doesn't close the session upon exiting. In the UI the session seems active but events are not working because the event handlers are not available. + /** Doesn't close the session upon exiting. In the UI the session seems active but events are not working because the event handlers are not available. Useful + * when we need to let the user read through some data. But no interaction is possible anymore between the user and the code. */ def leaveSessionOpenAfterExiting(): Unit = leaveSessionOpen.set(true) @@ -74,11 +88,19 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se onCloseHandler() case _ => + /** Normally this method shouldn't be called directly. Terminates any previous event iterators, clears the UI and renders the UiElements. + * @param es + * the UiElements to be rendered. + */ def render(es: Seq[UiElement]): Unit = clear() val j = toJson(es) sessionsService.setSessionJsonState(session, j) + /** Normally this method shouldn't be called directly. Renders updates to existing elements + * @param es + * a seq of updated elements, all these should already have been rendered before (but not necessarily their children) + */ def renderChanges(es: Seq[UiElement]): Unit = if !isClosed && es.nonEmpty then val j = toJson(es) From 46db38767f6c8ae06852eee026ce13b128d17dfe Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 26 Feb 2024 14:58:12 +0000 Subject: [PATCH 173/313] - --- .../org/terminal21/sparklib/SparkSessions.scala | 4 ---- .../sparklib/endtoend/SparkBasics.scala | 16 +++++++--------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/SparkSessions.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/SparkSessions.scala index 7bb3cb3d..26ee8ca3 100644 --- a/terminal21-spark/src/main/scala/org/terminal21/sparklib/SparkSessions.scala +++ b/terminal21-spark/src/main/scala/org/terminal21/sparklib/SparkSessions.scala @@ -1,10 +1,6 @@ package org.terminal21.sparklib import org.apache.spark.sql.SparkSession -import org.terminal21.client.components.ComponentLib -import org.terminal21.client.{ConnectedSession, Sessions} - -import scala.util.Using object SparkSessions: def newSparkSession( diff --git a/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala b/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala index aa273043..d1dfcc3a 100644 --- a/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala +++ b/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala @@ -50,21 +50,19 @@ import scala.util.Using data = Seq( Serie( "Scala", - data = Seq( - Datum("plane", 262), - Datum("helicopter", 26), - Datum("boat", 43) - ) + data = Nil ) ), axisBottom = Some(Axis(legend = "Class", legendOffset = 36)), - axisLeft = Some(Axis(legend = "Count", legendOffset = -40)), + axisLeft = Some(Axis(legend = "Number of Lines", legendOffset = -40)), legends = Seq(Legend()) ) - val sourceFileChart = sortedSourceFilesDS.visualize("Biggest Code Files", chart): results => - val data = results.take(10).map(cf => Datum(StringUtils.substringBeforeLast(cf.name, ".scala"), cf.numOfLines)).toList - chart.withData(Seq(Serie("Scala", data = data))) + val sourceFileChart = sourceFiles() + .sort($"numOfLines".desc) + .visualize("Biggest Code Files", chart): results => + val data = results.take(10).map(cf => Datum(StringUtils.substringBeforeLast(cf.name, ".scala"), cf.numOfLines)).toList + chart.withData(Seq(Serie("Scala", data = data))) Controller( Seq( From 3280e2eed56b2019d944d2b31d30f6acae81baba Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 26 Feb 2024 15:02:04 +0000 Subject: [PATCH 174/313] - --- .../scala/org/terminal21/sparklib/endtoend/SparkBasics.scala | 2 +- .../src/main/scala/org/terminal21/client/Controller.scala | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala b/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala index d1dfcc3a..e3298981 100644 --- a/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala +++ b/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala @@ -71,7 +71,7 @@ import scala.util.Using sortedCalcAsDF, sourceFileChart ) - ).render().lastEventOption + ).render().lastEventOptionOrNoneIfSessionIsClosed def sourceFiles()(using spark: SparkSession) = import scala3encoders.given diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 20ff5001..035a44cb 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -31,8 +31,8 @@ class Controller[M]( eventHandlers :+ handler ) - def lastEventOption: Option[M] = eventsIterator.lastOption - def eventsIterator: EventIterator[M] = new EventIterator(handledEventsIterator.takeWhile(!_.shouldTerminate).map(_.model)) + def lastEventOptionOrNoneIfSessionIsClosed: Option[M] = eventsIterator.lastOption + def eventsIterator: EventIterator[M] = new EventIterator(handledEventsIterator.takeWhile(!_.shouldTerminate).map(_.model)) private def clickHandlersMap(h: HandledEvent[M]): Map[String, Seq[OnClickEventHandlerFunction[M]]] = h.componentsByKey.values From 5375c5172f7fe262ed3deb4d3558c332981a1294 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 26 Feb 2024 15:04:11 +0000 Subject: [PATCH 175/313] - --- .../scala/org/terminal21/sparklib/endtoend/SparkBasics.scala | 2 +- .../src/main/scala/org/terminal21/client/Controller.scala | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala b/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala index e3298981..f719b8bd 100644 --- a/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala +++ b/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala @@ -71,7 +71,7 @@ import scala.util.Using sortedCalcAsDF, sourceFileChart ) - ).render().lastEventOptionOrNoneIfSessionIsClosed + ).render().eventsIterator.lastOption def sourceFiles()(using spark: SparkSession) = import scala3encoders.given diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 035a44cb..79a09eb7 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -31,8 +31,7 @@ class Controller[M]( eventHandlers :+ handler ) - def lastEventOptionOrNoneIfSessionIsClosed: Option[M] = eventsIterator.lastOption - def eventsIterator: EventIterator[M] = new EventIterator(handledEventsIterator.takeWhile(!_.shouldTerminate).map(_.model)) + def eventsIterator: EventIterator[M] = new EventIterator(handledEventsIterator.takeWhile(!_.shouldTerminate).map(_.model)) private def clickHandlersMap(h: HandledEvent[M]): Map[String, Seq[OnClickEventHandlerFunction[M]]] = h.componentsByKey.values From 58d45dfb2b4114b58eaf39916c34a25c0576729d Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 26 Feb 2024 15:11:54 +0000 Subject: [PATCH 176/313] - --- .../src/main/scala/tests/ChakraComponents.scala | 4 +--- .../src/main/scala/tests/MathJaxComponents.scala | 2 +- end-to-end-tests/src/main/scala/tests/NivoComponents.scala | 2 +- end-to-end-tests/src/main/scala/tests/RunAll.scala | 6 ++++++ .../src/main/scala/tests/StateSessionStateBug.scala | 4 ++-- end-to-end-tests/src/main/scala/tests/StdComponents.scala | 6 ++---- 6 files changed, 13 insertions(+), 11 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala index 8cb6630b..210992e1 100644 --- a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala @@ -3,10 +3,8 @@ package tests import org.terminal21.client.* import org.terminal21.client.components.UiElement import org.terminal21.client.components.chakra.* -import org.terminal21.client.components.std.Paragraph import tests.chakra.* -import java.util.concurrent.CountDownLatch import java.util.concurrent.atomic.AtomicBoolean @main def chakraComponents(): Unit = @@ -30,7 +28,7 @@ import java.util.concurrent.atomic.AtomicBoolean Overlay.components ++ Forms.components ++ Editables.components ++ Stacks.components ++ Grids.components ++ Buttons.components ++ Etc.components ++ MediaAndIcons.components ++ DataDisplay.components ++ Typography.components ++ Feedback.components ++ Disclosure.components ++ Navigation.components ++ Seq( krButton ) - Controller(components).eventsIterator.lastOption match + Controller(components).render().eventsIterator.lastOption match case Some(true) => loop() case _ => diff --git a/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala b/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala index 54bf44cb..449c99fd 100644 --- a/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala @@ -24,4 +24,4 @@ import org.terminal21.client.components.mathjax.* style = Map("backgroundColor" -> "gray") ) ) - Controller(components).eventsIterator.lastOption + Controller(components).render().eventsIterator.lastOption diff --git a/end-to-end-tests/src/main/scala/tests/NivoComponents.scala b/end-to-end-tests/src/main/scala/tests/NivoComponents.scala index 94cd0887..761d7d31 100644 --- a/end-to-end-tests/src/main/scala/tests/NivoComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/NivoComponents.scala @@ -13,4 +13,4 @@ import tests.nivo.{ResponsiveBarChart, ResponsiveLineChart} given ConnectedSession = session import Model.Standard.unitModel val components = ResponsiveBarChart() ++ ResponsiveLineChart() - Controller(components).eventsIterator.lastOption + Controller(components).render().eventsIterator.lastOption diff --git a/end-to-end-tests/src/main/scala/tests/RunAll.scala b/end-to-end-tests/src/main/scala/tests/RunAll.scala index a734bbf9..0523eb57 100644 --- a/end-to-end-tests/src/main/scala/tests/RunAll.scala +++ b/end-to-end-tests/src/main/scala/tests/RunAll.scala @@ -12,4 +12,10 @@ import org.terminal21.client.given , fiberExecutor.submit: loginFormApp() + , + fiberExecutor.submit: + mathJaxComponents() + , + fiberExecutor.submit: + nivoComponents() ).foreach(_.get()) diff --git a/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala b/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala index 7893d914..a4d1af7d 100644 --- a/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala +++ b/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala @@ -9,7 +9,7 @@ import java.util.Date @main def stateSessionStateBug(): Unit = Sessions - .withNewSession("state-session", "Stale Session") + .withNewSession("stale-session", "Stale Session") .connect: session => given ConnectedSession = session import Model.Standard.unitModel @@ -42,4 +42,4 @@ import java.util.Date Button(text = "Close").onClick: event => event.handled.terminate ) - Controller(components).eventsIterator.lastOption + Controller(components).render().eventsIterator.lastOption diff --git a/end-to-end-tests/src/main/scala/tests/StdComponents.scala b/end-to-end-tests/src/main/scala/tests/StdComponents.scala index ffee8ff4..be0dca05 100644 --- a/end-to-end-tests/src/main/scala/tests/StdComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/StdComponents.scala @@ -31,9 +31,7 @@ import org.terminal21.client.components.std.* NewLine(), Span(text = "And the last line") ), - Paragraph(text = "A Form").withChildren( - input - ), + Paragraph(text = "A Form").withChildren(input), output, Cookie(name = "std-components-test-cookie", value = "test-cookie-value"), CookieReader(name = "std-components-test-cookie").onChange: event => @@ -43,4 +41,4 @@ import org.terminal21.client.components.std.* cookieValue ) - Controller(components).eventsIterator.lastOption + Controller(components).render().eventsIterator.lastOption From 604a447bfd8da3f46aa684fab3ac3c39bc8f04df Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 26 Feb 2024 15:17:19 +0000 Subject: [PATCH 177/313] - --- .../src/main/scala/tests/ChakraComponents.scala | 12 ++++++------ .../src/main/scala/tests/RunAll.scala | 16 +++++++++++----- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala index 210992e1..7b4fb8a9 100644 --- a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala @@ -3,33 +3,33 @@ package tests import org.terminal21.client.* import org.terminal21.client.components.UiElement import org.terminal21.client.components.chakra.* +import org.terminal21.client.components.std.Paragraph import tests.chakra.* import java.util.concurrent.atomic.AtomicBoolean @main def chakraComponents(): Unit = - val keepRunning = new AtomicBoolean(true) - def loop(): Unit = println("Starting new session") Sessions .withNewSession("chakra-components", "Chakra Components") .connect: session => - keepRunning.set(false) given ConnectedSession = session given model: Model[Boolean] = Model(false) // react tests reset the session to clear state val krButton = Button(text = "Reset state").onClick: event => - keepRunning.set(true) - event.handled.terminate + event.handled.withModel(true).terminate val components: Seq[UiElement] = Overlay.components ++ Forms.components ++ Editables.components ++ Stacks.components ++ Grids.components ++ Buttons.components ++ Etc.components ++ MediaAndIcons.components ++ DataDisplay.components ++ Typography.components ++ Feedback.components ++ Disclosure.components ++ Navigation.components ++ Seq( krButton ) Controller(components).render().eventsIterator.lastOption match - case Some(true) => loop() + case Some(true) => + session.render(Seq(Paragraph(text = "resetting state"))) + Thread.sleep(500) + loop() case _ => loop() diff --git a/end-to-end-tests/src/main/scala/tests/RunAll.scala b/end-to-end-tests/src/main/scala/tests/RunAll.scala index 0523eb57..14e48976 100644 --- a/end-to-end-tests/src/main/scala/tests/RunAll.scala +++ b/end-to-end-tests/src/main/scala/tests/RunAll.scala @@ -1,21 +1,27 @@ package tests +import functions.fibers.Fiber import org.terminal21.client.given @main def runAll(): Unit = Seq( - fiberExecutor.submit: + submit: chakraComponents() , - fiberExecutor.submit: + submit: stdComponents() , - fiberExecutor.submit: + submit: loginFormApp() , - fiberExecutor.submit: + submit: mathJaxComponents() , - fiberExecutor.submit: + submit: nivoComponents() ).foreach(_.get()) + +private def submit(f: => Unit): Fiber[Unit] = + fiberExecutor.submit: + try f + catch case t: Throwable => t.printStackTrace() From 4e26684c426f4f3ff19ecc1d7077d16eddcb139b Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 26 Feb 2024 15:42:33 +0000 Subject: [PATCH 178/313] - --- end-to-end-tests/src/main/scala/tests/ChakraComponents.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala index 7b4fb8a9..a7d18187 100644 --- a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala @@ -27,7 +27,7 @@ import java.util.concurrent.atomic.AtomicBoolean ) Controller(components).render().eventsIterator.lastOption match case Some(true) => - session.render(Seq(Paragraph(text = "resetting state"))) + session.render(Seq(Paragraph(text = "chakra-session-reset"))) Thread.sleep(500) loop() case _ => From 00c16808fb0b81d1a3e91a7d7c9f07facba9420a Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 26 Feb 2024 15:43:44 +0000 Subject: [PATCH 179/313] - --- end-to-end-tests/src/main/scala/tests/ChakraComponents.scala | 2 -- 1 file changed, 2 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala index a7d18187..34ea85ec 100644 --- a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala @@ -6,8 +6,6 @@ import org.terminal21.client.components.chakra.* import org.terminal21.client.components.std.Paragraph import tests.chakra.* -import java.util.concurrent.atomic.AtomicBoolean - @main def chakraComponents(): Unit = def loop(): Unit = println("Starting new session") From 96e6626583e771d57e003e827d94c77567dce7ed Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 26 Feb 2024 17:38:25 +0000 Subject: [PATCH 180/313] - --- end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala | 3 ++- end-to-end-tests/src/main/scala/tests/NivoComponents.scala | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala b/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala index 449c99fd..dfeb044f 100644 --- a/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala @@ -24,4 +24,5 @@ import org.terminal21.client.components.mathjax.* style = Map("backgroundColor" -> "gray") ) ) - Controller(components).render().eventsIterator.lastOption + Controller(components).render() + session.leaveSessionOpenAfterExiting() diff --git a/end-to-end-tests/src/main/scala/tests/NivoComponents.scala b/end-to-end-tests/src/main/scala/tests/NivoComponents.scala index 761d7d31..35263516 100644 --- a/end-to-end-tests/src/main/scala/tests/NivoComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/NivoComponents.scala @@ -1,6 +1,5 @@ package tests -import org.terminal21.client.components.nivo.* import org.terminal21.client.* import org.terminal21.client.components.* import tests.nivo.{ResponsiveBarChart, ResponsiveLineChart} @@ -13,4 +12,5 @@ import tests.nivo.{ResponsiveBarChart, ResponsiveLineChart} given ConnectedSession = session import Model.Standard.unitModel val components = ResponsiveBarChart() ++ ResponsiveLineChart() - Controller(components).render().eventsIterator.lastOption + Controller(components).render() + session.leaveSessionOpenAfterExiting() From 1bfd8d3234dbd8c54b4e3a4093e68c6f7e7a0f56 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 26 Feb 2024 17:55:55 +0000 Subject: [PATCH 181/313] - --- example-spark/spark-notebook.sc | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/example-spark/spark-notebook.sc b/example-spark/spark-notebook.sc index e6383138..fc7c0718 100755 --- a/example-spark/spark-notebook.sc +++ b/example-spark/spark-notebook.sc @@ -28,6 +28,7 @@ Using.resource(SparkSessions.newSparkSession( /* configure your spark session he .connect: session => given ConnectedSession = session given SparkSession = spark + given Model[Unit] = Model.Standard.unitModel import scala3encoders.given import spark.implicits.* @@ -36,12 +37,12 @@ Using.resource(SparkSessions.newSparkSession( /* configure your spark session he val peopleDS = createPeople // We will display the data in a table - val peopleTable = QuickTable().headers("Id", "Name", "Age").caption("People") + val peopleTable = QuickTable().withHeaders("Id", "Name", "Age").withCaption("People") val peopleTableCalc = peopleDS .sort($"id") .visualize("People sample", peopleTable): data => - peopleTable.rows(data.take(5).map(p => Seq(p.id, p.name, p.age))) + peopleTable.withRows(data.take(5).map(p => Seq(p.id, p.name, p.age))) /** The calculation above uses a directory to store the dataset results. This way we can restart this script without loosing datasets that may take long * to calculate, making our script behave more like a notebook. When we click "Recalculate" in the UI, the cache directory is deleted and the dataset is @@ -69,7 +70,7 @@ Using.resource(SparkSessions.newSparkSession( /* configure your spark session he ) ) - Seq( + val components = Seq( Paragraph( text = """ |The spark notebooks can use the `visualise` extension method over a dataframe/dataset. It will cache the dataset by @@ -95,9 +96,8 @@ Using.resource(SparkSessions.newSparkSession( /* configure your spark session he ), peopleTableCalc, oldestPeopleChartCalc - ).render() - - session.waitTillUserClosesSession() + ) + Controller(components).render().eventsIterator.lastOption object SparkNotebook: private val names = Array("Andy", "Kostas", "Alex", "Andreas", "George", "Jack") From 4a9cb81da41976442b06bd565198a78ce6489566 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 27 Feb 2024 13:38:44 +0000 Subject: [PATCH 182/313] - --- example-scripts/bouncing-ball.sc | 5 +++-- example-scripts/csv-editor.sc | 37 ++++++++++++++++---------------- example-scripts/hello-world.sc | 3 ++- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/example-scripts/bouncing-ball.sc b/example-scripts/bouncing-ball.sc index e06824eb..b6cfef0a 100755 --- a/example-scripts/bouncing-ball.sc +++ b/example-scripts/bouncing-ball.sc @@ -19,15 +19,16 @@ Sessions .withNewSession("bouncing-ball", "C64 bouncing ball") .connect: session => given ConnectedSession = session + given Model[Unit] = Model.Standard.unitModel println( "Files under ~/.terminal21/web will be served under /web . Please place a ball.png file under ~/.terminal21/web/images on the box where the server runs." ) val ball = Image(src = "/web/images/ball.png") - ball.render() + session.render(Seq(ball)) @tailrec def animateBall(x: Int, y: Int, dx: Int, dy: Int): Unit = - ball.withStyle("position" -> "fixed", "left" -> (x + "px"), "top" -> (y + "px")).renderChanges() + session.renderChanges(Seq(ball.withStyle("position" -> "fixed", "left" -> (x + "px"), "top" -> (y + "px")))) Thread.sleep(1000 / 120) val newDx = if x < 0 || x > 600 then -dx else dx val newDy = if y < 0 || y > 500 then -dy else dy diff --git a/example-scripts/csv-editor.sc b/example-scripts/csv-editor.sc index ffb3f79f..5eba666a 100755 --- a/example-scripts/csv-editor.sc +++ b/example-scripts/csv-editor.sc @@ -6,9 +6,7 @@ // always import these import org.terminal21.client.* - import org.terminal21.client.components.* -import org.terminal21.client.model.* import org.terminal21.model.* // use the chakra components for menus, forms etc, https://chakra-ui.com/docs/components // The scala case classes : https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala @@ -39,8 +37,12 @@ Sessions editor.run() class CsvEditor(csv: Seq[Seq[String]])(using session: ConnectedSession): - val saveAndExit = Button(text = "Save & Exit") - val exit = Button(text = "Exit Without Saving") + case class CsvModel(save: Boolean, exitWithoutSave: Boolean) + private given Model[CsvModel] = Model(CsvModel(false, false)) + val saveAndExit = Button(text = "Save & Exit").onClick: event => + event.handled.withModel(true).terminate + val exit = Button(text = "Exit Without Saving").onClick: event => + event.handled.withModel(false).terminate val status = Box() val tableCells = @@ -49,11 +51,15 @@ class CsvEditor(csv: Seq[Seq[String]])(using session: ConnectedSession): newEditable(column) def run(): Unit = - components.render() - if processEvents then - save() - status.withText("Csv file saved, exiting.").renderChanges() - Thread.sleep(1000) + if controller.handledEventsIterator + .map: handled => + if handled.model then + handled.withRenderChanges(status.withText("Csv file saved, exiting.")) + Thread.sleep(500) + else handled + .toList + .lastOption + then save() def components: Seq[UiElement] = Seq( @@ -70,8 +76,8 @@ class CsvEditor(csv: Seq[Seq[String]])(using session: ConnectedSession): /** @return * true if the user clicked "Save", false if the user clicked "Exit" or closed the session */ - def processEvents: Boolean = - registerCsvEditorEventHandlers(Controller(false)).lastModelOption.getOrElse(false) + def controller: Controller[Boolean] = + Controller(components) def save(): Unit = val data = currentCsvValue @@ -85,12 +91,5 @@ class CsvEditor(csv: Seq[Seq[String]])(using session: ConnectedSession): EditablePreview(), EditableInput() ) - - def registerCsvEditorEventHandlers(controller: Controller[Boolean]) = - controller - .onClick(saveAndExit): event => - event.handled.withModel(true).terminate - .onClick(exit): click => - click.handled.withModel(false).terminate - .onChanged(tableCells.flatten*): event => + .onChange: event => event.handled.withRenderChanges(status.withText(s"Changed a cell value to ${event.newValue}")) diff --git a/example-scripts/hello-world.sc b/example-scripts/hello-world.sc index 43dd1d1d..f24a3e95 100755 --- a/example-scripts/hello-world.sc +++ b/example-scripts/hello-world.sc @@ -13,6 +13,7 @@ Sessions .withNewSession("hello-world", "Hello World Example") .connect: session => given ConnectedSession = session + given Model[Unit] = Model.Standard.unitModel // We don't have a model in this simple example - Paragraph(text = "Hello World!").render() + Controller(Seq(Paragraph(text = "Hello World!"))).render() session.leaveSessionOpenAfterExiting() From 25db9d23a80da433e95b81aa9c203abb67f51e99 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 27 Feb 2024 13:48:23 +0000 Subject: [PATCH 183/313] - --- .../main/scala/tests/ChakraComponents.scala | 2 +- .../src/main/scala/tests/LoginForm.scala | 4 +- .../scala/tests/StateSessionStateBug.scala | 2 +- .../src/main/scala/tests/StdComponents.scala | 2 +- .../src/test/scala/tests/LoggedInTest.scala | 11 ++-- .../src/test/scala/tests/LoginFormTest.scala | 4 +- .../serverapp/bundled/AppManager.scala | 3 +- .../serverapp/bundled/ServerStatusApp.scala | 2 +- .../serverapp/bundled/SettingsApp.scala | 2 +- .../bundled/AppManagerPageTest.scala | 4 +- .../org/terminal21/client/Controller.scala | 3 +- .../terminal21/client/ControllerTest.scala | 62 +++++++++++++------ 12 files changed, 63 insertions(+), 38 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala index 34ea85ec..3b211caf 100644 --- a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala @@ -23,7 +23,7 @@ import tests.chakra.* Overlay.components ++ Forms.components ++ Editables.components ++ Stacks.components ++ Grids.components ++ Buttons.components ++ Etc.components ++ MediaAndIcons.components ++ DataDisplay.components ++ Typography.components ++ Feedback.components ++ Disclosure.components ++ Navigation.components ++ Seq( krButton ) - Controller(components).render().eventsIterator.lastOption match + Controller(components).render().handledEventsIterator.lastOption.map(_.model) match case Some(true) => session.render(Seq(Paragraph(text = "chakra-session-reset"))) Thread.sleep(500) diff --git a/end-to-end-tests/src/main/scala/tests/LoginForm.scala b/end-to-end-tests/src/main/scala/tests/LoginForm.scala index 6f4ad872..8db58a88 100644 --- a/end-to-end-tests/src/main/scala/tests/LoginForm.scala +++ b/end-to-end-tests/src/main/scala/tests/LoginForm.scala @@ -45,7 +45,7 @@ class LoginForm(using session: ConnectedSession): val errorMsgInvalidEmail = Paragraph(text = "Invalid Email", style = Map("color" -> "red")) def run(): Option[Login] = - controller.render().eventsIterator.lastOptionOrNoneIfSessionClosed + controller.render().handledEventsIterator.lastOptionOrNoneIfSessionClosed.map(_.model) def components: Seq[UiElement] = Seq( @@ -91,7 +91,7 @@ class LoggedIn(login: Login)(using session: ConnectedSession): val passwordDetails = Text(text = s"password : ${login.pwd}") def run(): Option[Boolean] = - controller.render().eventsIterator.lastOption + controller.render().handledEventsIterator.lastOption.map(_.model) def components = Seq( diff --git a/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala b/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala index a4d1af7d..672ada6c 100644 --- a/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala +++ b/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala @@ -42,4 +42,4 @@ import java.util.Date Button(text = "Close").onClick: event => event.handled.terminate ) - Controller(components).render().eventsIterator.lastOption + Controller(components).render().handledEventsIterator.lastOption diff --git a/end-to-end-tests/src/main/scala/tests/StdComponents.scala b/end-to-end-tests/src/main/scala/tests/StdComponents.scala index be0dca05..6eca9052 100644 --- a/end-to-end-tests/src/main/scala/tests/StdComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/StdComponents.scala @@ -41,4 +41,4 @@ import org.terminal21.client.components.std.* cookieValue ) - Controller(components).render().eventsIterator.lastOption + Controller(components).render().handledEventsIterator.lastOption diff --git a/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala b/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala index 8b70aa42..ef3c7ee5 100644 --- a/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala +++ b/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala @@ -1,9 +1,8 @@ package tests import org.scalatest.funsuite.AnyFunSuiteLike -import org.terminal21.client.{ConnectedSession, ConnectedSessionMock} import org.scalatest.matchers.should.Matchers.* -import org.terminal21.client.components.* +import org.terminal21.client.{ConnectedSession, ConnectedSessionMock} import org.terminal21.model.CommandEvent class LoggedInTest extends AnyFunSuiteLike: @@ -23,12 +22,12 @@ class LoggedInTest extends AnyFunSuiteLike: test("yes clicked"): new App: - val eventsIt = form.controller.eventsIterator + val eventsIt = form.controller.handledEventsIterator session.fireEvents(CommandEvent.onClick(form.yesButton), CommandEvent.sessionClosed) - eventsIt.lastOption should be(Some(true)) + eventsIt.lastOption.map(_.model) should be(Some(true)) test("no clicked"): new App: - val eventsIt = form.controller.eventsIterator + val eventsIt = form.controller.handledEventsIterator session.fireEvents(CommandEvent.onClick(form.noButton), CommandEvent.sessionClosed) - eventsIt.lastOption should be(Some(false)) + eventsIt.lastOption.map(_.model) should be(Some(false)) diff --git a/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala b/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala index 5ed7d3f4..59450e4c 100644 --- a/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala +++ b/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala @@ -27,7 +27,7 @@ class LoginFormTest extends AnyFunSuiteLike: test("user submits validated data"): new App: - val eventsIt = form.controller.eventsIterator // get the iterator before we fire the events, otherwise the iterator will be empty + val eventsIt = form.controller.handledEventsIterator // get the iterator before we fire the events, otherwise the iterator will be empty session.fireEvents( CommandEvent.onChange(form.emailInput, "an@email.com"), CommandEvent.onChange(form.passwordInput, "secret"), @@ -35,7 +35,7 @@ class LoginFormTest extends AnyFunSuiteLike: CommandEvent.sessionClosed // every test should close the session so that the iterator doesn't block if converted to a list. ) - eventsIt.lastOption should be(Some(Login("an@email.com", "secret"))) + eventsIt.lastOption.map(_.model) should be(Some(Login("an@email.com", "secret"))) test("user submits invalid email"): new App: diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala index 60aa8bdf..c8e3b751 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala @@ -76,7 +76,8 @@ class AppManagerPage(apps: Seq[ServerSideApp], startApp: ServerSideApp => Unit)( def eventsIterator: Iterator[ManagerModel] = controller .render() - .eventsIterator + .handledEventsIterator + .map(_.model) .tapEach: m => for app <- m.startApp do startApp(app) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index cd18e313..0b0a5b72 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -33,7 +33,7 @@ class ServerStatusPage( while !appSession.isClosed do Thread.sleep(2000) appSession.fireEvents(Ticker) - controller(Runtime.getRuntime, sessionsService.allSessions).render().eventsIterator.lastOption + controller(Runtime.getRuntime, sessionsService.allSessions).render().handledEventsIterator.lastOption private def toMb(v: Long) = s"${v / (1024 * 1024)} MB" private val xs = Some("2xs") diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala index 94f97377..370870f3 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala @@ -22,7 +22,7 @@ class SettingsPage(using session: ConnectedSession): import Model.Standard.unitModel val themeToggle = ThemeToggle() def run() = - controller.render().eventsIterator.lastOption + controller.render().handledEventsIterator.lastOption def components = Seq(themeToggle) diff --git a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala index e865682b..8afc8e0d 100644 --- a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala +++ b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala @@ -57,6 +57,6 @@ class AppManagerPageTest extends AnyFunSuiteLike: val app = mockApp("app1", "the-app1-desc") new App(app): val other = Link() - val eventsIt = page.controller(page.components :+ other).eventsIterator + val eventsIt = page.controller(page.components :+ other).handledEventsIterator session.fireEvents(CommandEvent.onClick(page.appRows.head.head), CommandEvent.onClick(other), CommandEvent.sessionClosed) - eventsIt.toList.last.startApp should be(None) + eventsIt.toList.map(_.model).last.startApp should be(None) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 79a09eb7..0cc9fb37 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -31,8 +31,6 @@ class Controller[M]( eventHandlers :+ handler ) - def eventsIterator: EventIterator[M] = new EventIterator(handledEventsIterator.takeWhile(!_.shouldTerminate).map(_.model)) - private def clickHandlersMap(h: HandledEvent[M]): Map[String, Seq[OnClickEventHandlerFunction[M]]] = h.componentsByKey.values .collect: @@ -134,6 +132,7 @@ class Controller[M]( .flatMap: h => // trick to make sure we take the last state of the model when shouldTerminate=true if h.shouldTerminate then Seq(h.copy(shouldTerminate = false), h) else Seq(h) + .takeWhile(!_.shouldTerminate) ) object Controller: diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 12be6acb..c7a67cd7 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -33,7 +33,8 @@ class ControllerTest extends AnyFunSuiteLike: newController(model, Seq(buttonClick), Seq(button)) .onEvent: event => if event.model > 1 then event.handled.terminate else event.handled.withModel(event.model + 1) - .eventsIterator + .handledEventsIterator + .map(_.model) .toList should be(List(0, 1)) test("onEvent is called for change"): @@ -42,7 +43,8 @@ class ControllerTest extends AnyFunSuiteLike: .onEvent: event => import event.* if event.model > 1 then handled.terminate else handled.withModel(event.model + 1) - .eventsIterator + .handledEventsIterator + .map(_.model) .toList should be(List(0, 1)) test("onEvent not matched for change"): @@ -52,7 +54,8 @@ class ControllerTest extends AnyFunSuiteLike: case event: ControllerClickEvent[_] => import event.* handled.withModel(5) - .eventsIterator + .handledEventsIterator + .map(_.model) .toList should be(List(0, 0)) test("onEvent is called for change/boolean"): @@ -61,7 +64,8 @@ class ControllerTest extends AnyFunSuiteLike: .onEvent: event => import event.* if event.model > 1 then handled.terminate else handled.withModel(event.model + 1) - .eventsIterator + .handledEventsIterator + .map(_.model) .toList should be(List(0, 1)) test("onEvent not matches for change/boolean"): @@ -71,7 +75,8 @@ class ControllerTest extends AnyFunSuiteLike: case event: ControllerClickEvent[_] => import event.* handled.withModel(5) - .eventsIterator + .handledEventsIterator + .map(_.model) .toList should be(List(0, 0)) case class TestClientEvent(i: Int) extends ClientEvent @@ -83,7 +88,8 @@ class ControllerTest extends AnyFunSuiteLike: case ControllerClientEvent(handled, event: TestClientEvent) => import event.* handled.withModel(event.i).terminate - .eventsIterator + .handledEventsIterator + .map(_.model) .toList should be(List(0, 5)) test("onEvent when no partial function matches ClientEvent"): @@ -92,7 +98,8 @@ class ControllerTest extends AnyFunSuiteLike: .onEvent: case ControllerClickEvent(`checkbox`, handled) => handled.withModel(5).terminate - .eventsIterator + .handledEventsIterator + .map(_.model) .toList should be(List(0, 0)) test("onClick is called"): @@ -104,7 +111,9 @@ class ControllerTest extends AnyFunSuiteLike: button.onClick: event => event.handled.withModel(100).terminate ) - ).eventsIterator.toList should be(List(0, 100)) + ).handledEventsIterator + .map(_.model) + .toList should be(List(0, 100)) test("onChange is called"): given model: Model[Int] = Model(0) @@ -115,7 +124,9 @@ class ControllerTest extends AnyFunSuiteLike: input.onChange: event => event.handled.withModel(100).terminate ) - ).eventsIterator.toList should be(List(0, 100)) + ).handledEventsIterator + .map(_.model) + .toList should be(List(0, 100)) test("onChange/boolean is called"): given model: Model[Int] = Model(0) @@ -126,14 +137,17 @@ class ControllerTest extends AnyFunSuiteLike: checkbox.onChange: event => event.handled.withModel(100).terminate ) - ).eventsIterator.toList should be(List(0, 100)) + ).handledEventsIterator + .map(_.model) + .toList should be(List(0, 100)) test("terminate is obeyed and latest model state is iterated"): val model = Model(0) newController(model, Seq(buttonClick, buttonClick, buttonClick), Seq(button)) .onEvent: event => if event.model > 1 then event.handled.terminate.withModel(100) else event.handled.withModel(event.model + 1) - .eventsIterator + .handledEventsIterator + .map(_.model) .toList should be(List(0, 1, 2, 100)) test("changes are rendered"): @@ -143,7 +157,8 @@ class ControllerTest extends AnyFunSuiteLike: newController(Model(0), Seq(buttonClick), Seq(button), renderer) .onEvent: event => event.handled.withModel(event.model + 1).withRenderChanges(button.withText("changed")).terminate - .eventsIterator + .handledEventsIterator + .map(_.model) .toList should be(List(0, 1)) rendered should be(Seq(button.withText("changed"))) @@ -173,7 +188,8 @@ class ControllerTest extends AnyFunSuiteLike: newController(Model(0), Seq(buttonClick), Seq(button), renderer) .onEvent: event => event.handled.withModel(1).withTimedRenderChanges(TimedRenderChanges(10, button.withText("changed"))).terminate - .eventsIterator + .handledEventsIterator + .map(_.model) .toList should be(List(0, 1)) Thread.sleep(15) rendered should be(Seq(button.withText("changed"))) @@ -200,7 +216,9 @@ class ControllerTest extends AnyFunSuiteLike: button.onClick(using model): event => event.handled.withTimedRenderChanges(TimedRenderChanges(10, c)) ) - ).eventsIterator.toList should be(List(0, 0, 2)) + ).handledEventsIterator + .map(_.model) + .toList should be(List(0, 0, 2)) test("current value for OnChange"): val model = Model(0) @@ -212,7 +230,9 @@ class ControllerTest extends AnyFunSuiteLike: import event.* handled.withModel(if input.current.value == "new-value" then 100 else -1).terminate ) - ).eventsIterator.toList should be(List(0, 100)) + ).handledEventsIterator + .map(_.model) + .toList should be(List(0, 100)) test("current value for OnChange/boolean"): val model = Model(0) @@ -224,7 +244,9 @@ class ControllerTest extends AnyFunSuiteLike: import event.* handled.withModel(if checkbox.current.checked then 100 else -1).terminate ) - ).eventsIterator.toList should be(List(0, 100)) + ).handledEventsIterator + .map(_.model) + .toList should be(List(0, 100)) test("newly rendered elements are visible"): val model = Model(0) @@ -251,11 +273,15 @@ class ControllerTest extends AnyFunSuiteLike: lazy val box: Box = Box().withChildren(b) - newController(model, Seq(buttonClick, checkBoxChange), Seq(box)).eventsIterator.toList should be(List(0, 1, 2)) + newController(model, Seq(buttonClick, checkBoxChange), Seq(box)).handledEventsIterator + .map(_.model) + .toList should be(List(0, 1, 2)) test("RenderChangesEvent renders changes"): var rendered = Seq.empty[UiElement] def renderer(s: Seq[UiElement]): Unit = rendered = s - newController(Model(0), Seq(RenderChangesEvent(Seq(button.withText("changed")))), Seq(button), renderer).eventsIterator.toList should be(List(0, 0)) + newController(Model(0), Seq(RenderChangesEvent(Seq(button.withText("changed")))), Seq(button), renderer).handledEventsIterator + .map(_.model) + .toList should be(List(0, 0)) rendered should be(Seq(button.withText("changed"))) From 27f3da80f1e0d4cbb5f8ccb402080b3dd2666025 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 27 Feb 2024 13:51:07 +0000 Subject: [PATCH 184/313] - --- .../org/terminal21/serverapp/bundled/SettingsPageTest.scala | 3 +-- .../sparklib/calculations/StdUiSparkCalculationTest.scala | 2 +- .../scala/org/terminal21/sparklib/endtoend/SparkBasics.scala | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/SettingsPageTest.scala b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/SettingsPageTest.scala index 46f741d4..5a88f916 100644 --- a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/SettingsPageTest.scala +++ b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/SettingsPageTest.scala @@ -1,9 +1,8 @@ package org.terminal21.serverapp.bundled import org.scalatest.funsuite.AnyFunSuiteLike -import org.terminal21.client.{*, given} import org.scalatest.matchers.should.Matchers.* -import org.terminal21.model.CommandEvent +import org.terminal21.client.{*, given} class SettingsPageTest extends AnyFunSuiteLike: class App: diff --git a/terminal21-spark/src/test/scala/org/terminal21/sparklib/calculations/StdUiSparkCalculationTest.scala b/terminal21-spark/src/test/scala/org/terminal21/sparklib/calculations/StdUiSparkCalculationTest.scala index e363c9e0..7a32d07b 100644 --- a/terminal21-spark/src/test/scala/org/terminal21/sparklib/calculations/StdUiSparkCalculationTest.scala +++ b/terminal21-spark/src/test/scala/org/terminal21/sparklib/calculations/StdUiSparkCalculationTest.scala @@ -85,7 +85,7 @@ class StdUiSparkCalculationTest extends AnyFunSuiteLike with Eventually: given SparkSession = spark val calc = new TestingCalculation calc.run().get().collect().toList should be(List(2)) - val it = Controller(Seq(calc)).eventsIterator + val it = Controller(Seq(calc)).handledEventsIterator session.fireClickEvent(calc.recalc) session.fireSessionClosedEvent() it.lastOption diff --git a/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala b/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala index f719b8bd..600fc8cf 100644 --- a/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala +++ b/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala @@ -71,7 +71,7 @@ import scala.util.Using sortedCalcAsDF, sourceFileChart ) - ).render().eventsIterator.lastOption + ).render().handledEventsIterator.lastOption def sourceFiles()(using spark: SparkSession) = import scala3encoders.given From a4f0cc7dbb05dbe7d6342499de85910a8b626f7a Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 27 Feb 2024 14:28:57 +0000 Subject: [PATCH 185/313] - --- example-scripts/csv-editor.sc | 36 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/example-scripts/csv-editor.sc b/example-scripts/csv-editor.sc index 5eba666a..7f9dc78a 100755 --- a/example-scripts/csv-editor.sc +++ b/example-scripts/csv-editor.sc @@ -37,12 +37,17 @@ Sessions editor.run() class CsvEditor(csv: Seq[Seq[String]])(using session: ConnectedSession): - case class CsvModel(save: Boolean, exitWithoutSave: Boolean) - private given Model[CsvModel] = Model(CsvModel(false, false)) + case class CsvModel(save: Boolean, exitWithoutSave: Boolean, csv: Seq[Seq[String]]) + private given Model[CsvModel] = Model(CsvModel(false, false, Nil)) val saveAndExit = Button(text = "Save & Exit").onClick: event => - event.handled.withModel(true).terminate + import event.* + val csv = tableCells.map: row => + row.map: editable => + editable.current.value + handled.withModel(CsvModel(true, false, csv)).terminate val exit = Button(text = "Exit Without Saving").onClick: event => - event.handled.withModel(false).terminate + import event.* + handled.withModel(model.copy(exitWithoutSave = true)).terminate val status = Box() val tableCells = @@ -51,15 +56,8 @@ class CsvEditor(csv: Seq[Seq[String]])(using session: ConnectedSession): newEditable(column) def run(): Unit = - if controller.handledEventsIterator - .map: handled => - if handled.model then - handled.withRenderChanges(status.withText("Csv file saved, exiting.")) - Thread.sleep(500) - else handled - .toList - .lastOption - then save() + for handled <- controller.render().handledEventsIterator.lastOption.filter(_.model.save) + do save(handled.model.csv) def components: Seq[UiElement] = Seq( @@ -76,16 +74,14 @@ class CsvEditor(csv: Seq[Seq[String]])(using session: ConnectedSession): /** @return * true if the user clicked "Save", false if the user clicked "Exit" or closed the session */ - def controller: Controller[Boolean] = + def controller: Controller[CsvModel] = Controller(components) - def save(): Unit = - val data = currentCsvValue - FileUtils.writeStringToFile(file, data, "UTF-8") + def save(data: Seq[Seq[String]]): Unit = + FileUtils.writeStringToFile(file, data.map(_.mkString(",")).mkString("\n"), "UTF-8") + println(s"Csv file saved to $file") - def currentCsvValue: String = tableCells.map(_.map(_.current.value).mkString(",")).mkString("\n") - - private def newEditable(value: String) = + private def newEditable(value: String): Editable = Editable(defaultValue = value) .withChildren( EditablePreview(), From fc960463073c9d700cb7ac4b4f827d5ae8c4e263 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 27 Feb 2024 14:40:43 +0000 Subject: [PATCH 186/313] - --- example-scripts/csv-viewer.sc | 32 ++++++++++--------- .../org/terminal21/client/Controller.scala | 2 ++ 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/example-scripts/csv-viewer.sc b/example-scripts/csv-viewer.sc index 24c5e5ac..5a38521a 100755 --- a/example-scripts/csv-viewer.sc +++ b/example-scripts/csv-viewer.sc @@ -31,22 +31,24 @@ Sessions .withNewSession(s"csv-viewer-$fileName", s"CsvView: $fileName") .connect: session => given ConnectedSession = session - - TableContainer() - .withChildren( - Table(variant = "striped", colorScheme = Some("teal"), size = "mg") - .withChildren( - TableCaption(text = "Csv file contents"), - Tbody( - children = csv.map: row => - Tr( - children = row.map: column => - Td(text = column) - ) + given Model[Unit] = Model.Standard.unitModel + + Controller( + TableContainer() + .withChildren( + Table(variant = "striped", colorScheme = Some("teal"), size = "mg") + .withChildren( + TableCaption(text = "Csv file contents"), + Tbody( + children = csv.map: row => + Tr( + children = row.map: column => + Td(text = column) + ) + ) ) - ) - ) - .render() + ) + ).render() // we don't have to process any events here, just let the user view the csv file. println(s"Now open ${session.uiUrl} to view the UI.") // since this is a read-only UI, we can exit the app but leave the session open on the UI for the user to examine the data. session.leaveSessionOpenAfterExiting() diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 0cc9fb37..d3741fb5 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -146,6 +146,8 @@ object Controller: new Controller(session.eventIterator, session.fireEvent, session.renderChanges, components, initialModel) def apply[M](components: Seq[UiElement])(using initialModel: Model[M], session: ConnectedSession): Controller[M] = new Controller(session.eventIterator, session.fireEvent, session.renderChanges, components, initialModel) + def apply[M](component: UiElement)(using initialModel: Model[M], session: ConnectedSession): Controller[M] = + apply(Seq(component)) sealed trait ControllerEvent[M]: def model: M = handled.model From 5c85d8cb0ea4b8aacaf6d364420916145e242d94 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 27 Feb 2024 14:53:14 +0000 Subject: [PATCH 187/313] - --- example-scripts/hello-world.sc | 5 ++++- example-scripts/mathjax.sc | 18 ++++++++++++------ example-scripts/nivo-bar-chart.sc | 25 ++++++++++++++++++------- 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/example-scripts/hello-world.sc b/example-scripts/hello-world.sc index f24a3e95..eecaf3b1 100755 --- a/example-scripts/hello-world.sc +++ b/example-scripts/hello-world.sc @@ -9,11 +9,14 @@ import org.terminal21.client.components.* // std components, https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdElement.scala import org.terminal21.client.components.std.* +// We don't have a model in this simple example, so we will import the standard Unit model +// for our controller to use. +import org.terminal21.client.Model.Standard.unitModel + Sessions .withNewSession("hello-world", "Hello World Example") .connect: session => given ConnectedSession = session - given Model[Unit] = Model.Standard.unitModel // We don't have a model in this simple example Controller(Seq(Paragraph(text = "Hello World!"))).render() session.leaveSessionOpenAfterExiting() diff --git a/example-scripts/mathjax.sc b/example-scripts/mathjax.sc index 5a4a0cee..8e65f2e5 100755 --- a/example-scripts/mathjax.sc +++ b/example-scripts/mathjax.sc @@ -4,17 +4,22 @@ import org.terminal21.client.* import org.terminal21.client.components.* import org.terminal21.client.components.mathjax.* +// We don't have a model in this simple example, so we will import the standard Unit model +// for our controller to use. +import org.terminal21.client.Model.Standard.unitModel + Sessions .withNewSession("mathjax", "MathJax Example") .andLibraries(MathJaxLib /* note we need to register the MathJaxLib in order to use it */ ) .connect: session => given ConnectedSession = session - Seq( - MathJax( - expression = """When \(a \ne 0\), there are two solutions to \(ax^2 + bx + c = 0\) and they are $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$""" - ), - MathJax( - expression = """ + Controller( + Seq( + MathJax( + expression = """When \(a \ne 0\), there are two solutions to \(ax^2 + bx + c = 0\) and they are $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$""" + ), + MathJax( + expression = """ |when \(a \ne 0\), there are two solutions to \(x = {-b \pm \sqrt{b^2-4ac} \over 2a}.\) |Aenean vel velit a lacus lacinia pulvinar. Morbi eget ex et tellus aliquam molestie sit amet eu diam. |Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas tellus enim, tempor non efficitur et, rutrum efficitur metus. @@ -24,6 +29,7 @@ Sessions |Morbi ultrices sem quis nisl convallis, ac cursus nunc condimentum. Orci varius natoque penatibus et magnis dis parturient montes, |nascetur ridiculus mus. |""".stripMargin + ) ) ).render() session.leaveSessionOpenAfterExiting() diff --git a/example-scripts/nivo-bar-chart.sc b/example-scripts/nivo-bar-chart.sc index 2a15732e..6be19f73 100755 --- a/example-scripts/nivo-bar-chart.sc +++ b/example-scripts/nivo-bar-chart.sc @@ -8,6 +8,10 @@ import org.terminal21.client.components.nivo.* import scala.util.Random import NivoBarChart.* +import org.terminal21.model.ClientEvent +// We don't have a model in this simple example, so we will import the standard Unit model +// for our controller to use. +import org.terminal21.client.Model.Standard.unitModel Sessions .withNewSession("nivo-bar-chart", "Nivo Bar Chart") @@ -42,17 +46,24 @@ Sessions ) ) - Seq( - Paragraph(text = "Various foods.", style = Map("margin" -> 20)), - chart - ).render() - + // we'll send new data to our controller every 2 seconds via a custom event + case class NewData(data: Seq[Seq[BarDatum]]) extends ClientEvent fiberExecutor.submit: while !session.isClosed do Thread.sleep(2000) - chart.withData(createRandomData).renderChanges() + session.fireEvent(NewData(createRandomData)) - session.waitTillUserClosesSession() + Controller( + Seq( + Paragraph(text = "Various foods.", style = Map("margin" -> 20)), + chart + ) + ).render() + .onEvent: // receive the new data and render them + case ControllerClientEvent(handled, NewData(data)) => + handled.withRenderChanges(chart.withData(data)) + .handledEventsIterator + .lastOption object NivoBarChart: def createRandomData: Seq[Seq[BarDatum]] = From 46adff4c980021dfc02c85a0fd4577948a5bd56a Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 27 Feb 2024 15:06:00 +0000 Subject: [PATCH 188/313] - --- example-scripts/csv-editor.sc | 10 +++++++--- example-scripts/nivo-line-chart.sc | 25 ++++++++++++++++++------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/example-scripts/csv-editor.sc b/example-scripts/csv-editor.sc index 7f9dc78a..c41fb93b 100755 --- a/example-scripts/csv-editor.sc +++ b/example-scripts/csv-editor.sc @@ -26,7 +26,7 @@ val contents = if file.exists() then FileUtils.readFileToString(file, "UTF-8") else "type,damage points,hit points\nmage,10dp,20hp\nwarrior,20dp,30hp" -val csv = contents.split("\n").map(_.split(",").toSeq).toSeq +val csv: Seq[Seq[String]] = contents.split("\n").map(_.split(",").toSeq).toSeq Sessions .withNewSession(s"csv-editor-$fileName", s"CsvEdit: $fileName") @@ -37,14 +37,18 @@ Sessions editor.run() class CsvEditor(csv: Seq[Seq[String]])(using session: ConnectedSession): + /** Our model. If the user clicks "Save", we'll set `save` to true and store the csv data into `csv` + */ case class CsvModel(save: Boolean, exitWithoutSave: Boolean, csv: Seq[Seq[String]]) + private given Model[CsvModel] = Model(CsvModel(false, false, Nil)) + val saveAndExit = Button(text = "Save & Exit").onClick: event => import event.* val csv = tableCells.map: row => row.map: editable => editable.current.value - handled.withModel(CsvModel(true, false, csv)).terminate + handled.withModel(CsvModel(true, false, csv)).terminate // terminate the event iteration after storing the data into the model val exit = Button(text = "Exit Without Saving").onClick: event => import event.* handled.withModel(model.copy(exitWithoutSave = true)).terminate @@ -56,7 +60,7 @@ class CsvEditor(csv: Seq[Seq[String]])(using session: ConnectedSession): newEditable(column) def run(): Unit = - for handled <- controller.render().handledEventsIterator.lastOption.filter(_.model.save) + for handled <- controller.render().handledEventsIterator.lastOption.filter(_.model.save) // only save if model.save is true do save(handled.model.csv) def components: Seq[UiElement] = diff --git a/example-scripts/nivo-line-chart.sc b/example-scripts/nivo-line-chart.sc index e44be1b9..963b0bee 100755 --- a/example-scripts/nivo-line-chart.sc +++ b/example-scripts/nivo-line-chart.sc @@ -8,6 +8,10 @@ import org.terminal21.client.components.nivo.* import scala.util.Random import NivoLineChart.* +import org.terminal21.model.ClientEvent +// We don't have a model in this simple example, so we will import the standard Unit model +// for our controller to use. +import org.terminal21.client.Model.Standard.unitModel Sessions .withNewSession("nivo-line-chart", "Nivo Line Chart") @@ -23,17 +27,24 @@ Sessions legends = Seq(Legend()) ) - Seq( - Paragraph(text = "Means of transportation for various countries", style = Map("margin" -> 20)), - chart - ).render() - + // we'll send new data to our controller every 2 seconds via a custom event + case class NewData(data: Seq[Serie]) extends ClientEvent fiberExecutor.submit: while !session.isClosed do Thread.sleep(2000) - chart.withData(createRandomData).renderChanges() + session.fireEvent(NewData(createRandomData)) - session.waitTillUserClosesSession() + Controller( + Seq( + Paragraph(text = "Means of transportation for various countries", style = Map("margin" -> 20)), + chart + ) + ).render() + .onEvent: // receive the new data and render them + case ControllerClientEvent(handled, NewData(data)) => + handled.withRenderChanges(chart.withData(data)) + .handledEventsIterator + .lastOption object NivoLineChart: def createRandomData: Seq[Serie] = From f16629a6a3b11046c07a855a8f0a13afdf6b1621 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 27 Feb 2024 15:37:41 +0000 Subject: [PATCH 189/313] - --- Readme.md | 2 +- example-scripts/on-change.sc | 43 +++++++++++++++++++++++------------- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/Readme.md b/Readme.md index 87095574..b72c471f 100644 --- a/Readme.md +++ b/Readme.md @@ -170,7 +170,7 @@ Please use the [discussions](https://github.com/kostaskougios/terminal21-restapi - apps can now run on the server + server management bundled apps - Cookie setter and reader. - session builders refactoring for more flexible creation of sessions -- QuickTabs +- QuickTabs, QuickFormControl - bug fix for old react state re-rendering on new session - event iterators allows idiomatic handling of events and overhaul of the event handling for easier testing diff --git a/example-scripts/on-change.sc b/example-scripts/on-change.sc index 3c8f93a2..82cdf813 100755 --- a/example-scripts/on-change.sc +++ b/example-scripts/on-change.sc @@ -5,25 +5,38 @@ import org.terminal21.client.components.* import org.terminal21.client.components.std.Paragraph import org.terminal21.client.components.chakra.* +case class UserForm(email: String, submitted: Boolean) + Sessions .withNewSession("on-change-example", "On Change event handler") .connect: session => given ConnectedSession = session + val user = UserForm("my@email.com", false) + given Model[UserForm] = Model(user) val output = Paragraph(text = "Please modify the email.") - val email = Input(`type` = "email", defaultValue = "my@email.com").onChange: v => - output.withText(s"Email value : $v").renderChanges() - - Seq( - FormControl().withChildren( - FormLabel(text = "Email address"), - InputGroup().withChildren( - InputLeftAddon().withChildren(EmailIcon()), - email - ), - FormHelperText(text = "We'll never share your email.") - ), - output - ).render() + val email = Input(`type` = "email", defaultValue = user.email).onChange: event => + import event.* + val v = event.newValue + handled.withModel(model.copy(email = v)).withRenderChanges(output.withText(s"Email value : $v")) - session.waitTillUserClosesSession() + Controller( + Seq( + QuickFormControl() + .withLabel("Email address") + .withInputGroup( + InputLeftAddon().withChildren(EmailIcon()), + email + ) + .withHelperText("We'll never share your email."), + Button(text = "Submit").onClick: event => + import event.* + handled.withModel(model.copy(submitted = true)).terminate // mark the form as submitted and terminate the event iteration + , + output + ) + ).render().handledEventsIterator.lastOption.map(_.model).filter(_.submitted) match + case Some(submittedUser) => + println(s"Submitted: $submittedUser") + case None => + println("User closed session without submitting the form") From d6296ac275e323c7903b8e8df79002e161ef3700 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 27 Feb 2024 15:50:21 +0000 Subject: [PATCH 190/313] - --- example-scripts/on-change.sc | 66 +++++++++++++++++++++++------------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/example-scripts/on-change.sc b/example-scripts/on-change.sc index 82cdf813..8628da6b 100755 --- a/example-scripts/on-change.sc +++ b/example-scripts/on-change.sc @@ -5,38 +5,58 @@ import org.terminal21.client.components.* import org.terminal21.client.components.std.Paragraph import org.terminal21.client.components.chakra.* -case class UserForm(email: String, submitted: Boolean) +case class UserForm( + email: String, // the email + submitted: Boolean // true if user clicks the submit button, false otherwise +) Sessions .withNewSession("on-change-example", "On Change event handler") .connect: session => given ConnectedSession = session - val user = UserForm("my@email.com", false) - given Model[UserForm] = Model(user) + new UserPage(UserForm("my@email.com", false)).run match + case Some(submittedUser) => + println(s"Submitted: $submittedUser") + case None => + println("User closed session without submitting the form") + +/** One nice way to structure the code (that simplifies testing too) is to create a class for every page in the user interface. In this instance, we create a + * page for the user form to be displayed. All components are in `components` method. The controller is in the `controller` method and we can run to get the + * result in the `run` method. We can use these methods in unit tests to test what is rendered and how events are processed respectively. + */ +class UserPage(user: UserForm)(using ConnectedSession): + given Model[UserForm] = Model(user) // the Model for our page. This is given so that we can handle events and create the controller. + + /** Runs the form and returns the results + * @return + * if None, the user didn't submit the form (i.e. closed the session), if Some(userForm) the user submitted the form. + */ + def run: Option[UserForm] = + controller.render().handledEventsIterator.lastOption.map(_.model).filter(_.submitted) + /** @return + * all the components that should be rendered for the page + */ + def components: Seq[UiElement] = val output = Paragraph(text = "Please modify the email.") val email = Input(`type` = "email", defaultValue = user.email).onChange: event => import event.* val v = event.newValue handled.withModel(model.copy(email = v)).withRenderChanges(output.withText(s"Email value : $v")) - Controller( - Seq( - QuickFormControl() - .withLabel("Email address") - .withInputGroup( - InputLeftAddon().withChildren(EmailIcon()), - email - ) - .withHelperText("We'll never share your email."), - Button(text = "Submit").onClick: event => - import event.* - handled.withModel(model.copy(submitted = true)).terminate // mark the form as submitted and terminate the event iteration - , - output - ) - ).render().handledEventsIterator.lastOption.map(_.model).filter(_.submitted) match - case Some(submittedUser) => - println(s"Submitted: $submittedUser") - case None => - println("User closed session without submitting the form") + Seq( + QuickFormControl() + .withLabel("Email address") + .withInputGroup( + InputLeftAddon().withChildren(EmailIcon()), + email + ) + .withHelperText("We'll never share your email."), + Button(text = "Submit").onClick: event => + import event.* + handled.withModel(model.copy(submitted = true)).terminate // mark the form as submitted and terminate the event iteration + , + output + ) + + def controller: Controller[UserForm] = Controller(components) From 8233cb93ae200882daddb74c9faca34380d81b84 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 27 Feb 2024 16:22:58 +0000 Subject: [PATCH 191/313] - --- example-scripts/on-click.sc | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/example-scripts/on-click.sc b/example-scripts/on-click.sc index 6685cfb8..5f443fd9 100755 --- a/example-scripts/on-click.sc +++ b/example-scripts/on-click.sc @@ -6,21 +6,23 @@ import org.terminal21.client.components.std.* import org.terminal21.client.components.chakra.* import org.terminal21.model.SessionOptions +case class ClickForm(clicked: Boolean) + Sessions .withNewSession("on-click-example", "On Click Handler") - .andOptions(SessionOptions.LeaveOpenWhenTerminated) // leave the app UI visible after we terminate so that we can see the final "Button clicked" message .connect: session => given ConnectedSession = session + given Model[ClickForm] = Model(ClickForm(false)) val msg = Paragraph(text = "Waiting for user to click the button") - val button = Button(text = "Please click me") - Seq(msg, button).render() - - case class Model(isButtonClicked: Boolean) + val button = Button(text = "Please click me").onClick: event => + import event.* + handled.withModel(model.copy(clicked = true)).withRenderChanges(msg.withText("Button clicked")).terminate - session.eventIterator // get an iterator for all events - .map(e => Model(e.isTarget(button))) // convert it to our model - .dropWhile(!_.isButtonClicked) // ignore all events until the user clicks our button - .nextOption() match + Controller( + Seq(msg, button) + ).render().handledEventsIterator.lastOption.map(_.model) match case None => // the user closed the app - case Some(model) => msg.withText(s"Button clicked. Model = $model").renderChanges() // the user for sure clicked the button + case Some(model) => println(s"model = $model") + + session.leaveSessionOpenAfterExiting() // leave the session open after exiting so that the user can examine the UI From 3ed96aec274464d5310f5c89de2bc4a9fa8b59d7 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 27 Feb 2024 16:29:32 +0000 Subject: [PATCH 192/313] - --- example-scripts/on-click.sc | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/example-scripts/on-click.sc b/example-scripts/on-click.sc index 5f443fd9..a67cc501 100755 --- a/example-scripts/on-click.sc +++ b/example-scripts/on-click.sc @@ -12,17 +12,26 @@ Sessions .withNewSession("on-click-example", "On Click Handler") .connect: session => given ConnectedSession = session - given Model[ClickForm] = Model(ClickForm(false)) + new ClickPage(ClickForm(false)).run match + case None => // the user closed the app + case Some(model) => println(s"model = $model") + + session.leaveSessionOpenAfterExiting() // leave the session open after exiting so that the user can examine the UI + +/** One nice way to structure the code (that simplifies testing too) is to create a class for every page in the user interface. In this instance, we create a + * page for the click form to be displayed. All components are in `components` method. The controller is in the `controller` method and we can run to get the + * result in the `run` method. We can use these methods in unit tests to test what is rendered and how events are processed respectively. + */ +class ClickPage(clickForm: ClickForm)(using ConnectedSession): + given Model[ClickForm] = Model(clickForm) + def run = controller.render().handledEventsIterator.lastOption.map(_.model) + + def components: Seq[UiElement] = val msg = Paragraph(text = "Waiting for user to click the button") val button = Button(text = "Please click me").onClick: event => import event.* handled.withModel(model.copy(clicked = true)).withRenderChanges(msg.withText("Button clicked")).terminate + Seq(msg, button) - Controller( - Seq(msg, button) - ).render().handledEventsIterator.lastOption.map(_.model) match - case None => // the user closed the app - case Some(model) => println(s"model = $model") - - session.leaveSessionOpenAfterExiting() // leave the session open after exiting so that the user can examine the UI + def controller: Controller[ClickForm] = Controller(components) From d355348835716a1f72f89c645c30feee1dd762c5 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 27 Feb 2024 16:35:41 +0000 Subject: [PATCH 193/313] - --- example-scripts/postit.sc | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/example-scripts/postit.sc b/example-scripts/postit.sc index e84ffa37..a3059279 100755 --- a/example-scripts/postit.sc +++ b/example-scripts/postit.sc @@ -16,25 +16,33 @@ Sessions .withNewSession("postit", "Post-It") .connect: session => given ConnectedSession = session + println(s"Now open ${session.uiUrl} to view the UI.") + new PostItPage().run() + +class PostItPage(using ConnectedSession): + import org.terminal21.client.Model.Standard.unitModel // we won't use a model for this one + def run(): Unit = controller.render().handledEventsIterator.lastOption + def components: Seq[UiElement] = val editor = Textarea(placeholder = "Please post your note by clicking here and editing the content") val messages = VStack(align = Some("stretch")) - val add = Button(text = "Post It.").onClick: () => + val add = Button(text = "Post It.").onClick: event => + import event.* // add the new msg. // note: editor.value is automatically updated by terminal-ui val currentMessages = messages.current - currentMessages - .addChildren( - HStack().withChildren( - Image( - src = "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fa/Apple_Notes_icon.svg/2048px-Apple_Notes_icon.svg.png", - boxSize = Some("32px") - ), - Box(text = editor.current.value) + handled.withRenderChanges( + currentMessages + .addChildren( + HStack().withChildren( + Image( + src = "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fa/Apple_Notes_icon.svg/2048px-Apple_Notes_icon.svg.png", + boxSize = Some("32px") + ), + Box(text = editor.current.value) + ) ) - ) - .renderChanges() - + ) Seq( Paragraph(text = "Please type your note below and click 'Post It' to post it so that everyone can view it."), InputGroup().withChildren( @@ -43,7 +51,6 @@ Sessions ), add, messages - ).render() + ) - println(s"Now open ${session.uiUrl} to view the UI.") - session.waitTillUserClosesSession() + def controller = Controller(components) From 1fa79f5480a87ad197921d5ee0cd7198b3e7b89c Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 27 Feb 2024 16:44:20 +0000 Subject: [PATCH 194/313] - --- .../src/test/scala/tests/LoggedInTest.scala | 4 +- .../src/test/scala/tests/LoginFormTest.scala | 4 +- .../bundled/AppManagerPageTest.scala | 2 +- .../bundled/ServerStatusPageTest.scala | 2 +- .../StdUiSparkCalculationTest.scala | 2 +- .../org/terminal21/client/Controller.scala | 25 ++++++--- .../terminal21/client/ControllerTest.scala | 55 +++++++++++++------ 7 files changed, 61 insertions(+), 33 deletions(-) diff --git a/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala b/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala index ef3c7ee5..e7806a45 100644 --- a/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala +++ b/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala @@ -22,12 +22,12 @@ class LoggedInTest extends AnyFunSuiteLike: test("yes clicked"): new App: - val eventsIt = form.controller.handledEventsIterator + val eventsIt = form.controller.render().handledEventsIterator session.fireEvents(CommandEvent.onClick(form.yesButton), CommandEvent.sessionClosed) eventsIt.lastOption.map(_.model) should be(Some(true)) test("no clicked"): new App: - val eventsIt = form.controller.handledEventsIterator + val eventsIt = form.controller.render().handledEventsIterator session.fireEvents(CommandEvent.onClick(form.noButton), CommandEvent.sessionClosed) eventsIt.lastOption.map(_.model) should be(Some(false)) diff --git a/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala b/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala index 59450e4c..13b93ac1 100644 --- a/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala +++ b/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala @@ -27,7 +27,7 @@ class LoginFormTest extends AnyFunSuiteLike: test("user submits validated data"): new App: - val eventsIt = form.controller.handledEventsIterator // get the iterator before we fire the events, otherwise the iterator will be empty + val eventsIt = form.controller.render().handledEventsIterator // get the iterator before we fire the events, otherwise the iterator will be empty session.fireEvents( CommandEvent.onChange(form.emailInput, "an@email.com"), CommandEvent.onChange(form.passwordInput, "secret"), @@ -39,7 +39,7 @@ class LoginFormTest extends AnyFunSuiteLike: test("user submits invalid email"): new App: - val eventsIt = form.controller.handledEventsIterator // get the iterator that iterates Handled instances so that we can assert on renderChanges + val eventsIt = form.controller.render().handledEventsIterator // get the iterator that iterates Handled instances so that we can assert on renderChanges session.fireEvents( CommandEvent.onChange(form.emailInput, "invalid-email.com"), CommandEvent.onClick(form.submitButton), diff --git a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala index 8afc8e0d..5eb11da2 100644 --- a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala +++ b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala @@ -57,6 +57,6 @@ class AppManagerPageTest extends AnyFunSuiteLike: val app = mockApp("app1", "the-app1-desc") new App(app): val other = Link() - val eventsIt = page.controller(page.components :+ other).handledEventsIterator + val eventsIt = page.controller(page.components :+ other).render().handledEventsIterator session.fireEvents(CommandEvent.onClick(page.appRows.head.head), CommandEvent.onClick(other), CommandEvent.sessionClosed) eventsIt.toList.map(_.model).last.startApp should be(None) diff --git a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala index d9e53d87..417fd896 100644 --- a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala +++ b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala @@ -63,7 +63,7 @@ class ServerStatusPageTest extends AnyFunSuiteLike: case 1 => Seq(session(id = "s1", name = "session 1")) // this is initially rendered case 2 => Seq(session(id = "s2", name = "session 2")) // this is a change case 3 => Seq(session(id = "s3", name = "session 3")) // this is also a change - val it = page.controller(Runtime.getRuntime, sessions).handledEventsIterator + val it = page.controller(Runtime.getRuntime, sessions).render().handledEventsIterator connectedSession.fireEvents(page.Ticker, page.Ticker, CommandEvent.sessionClosed) val handledEvents = it.toList handledEvents.head.renderChanges should be(Nil) diff --git a/terminal21-spark/src/test/scala/org/terminal21/sparklib/calculations/StdUiSparkCalculationTest.scala b/terminal21-spark/src/test/scala/org/terminal21/sparklib/calculations/StdUiSparkCalculationTest.scala index 7a32d07b..3a567b4b 100644 --- a/terminal21-spark/src/test/scala/org/terminal21/sparklib/calculations/StdUiSparkCalculationTest.scala +++ b/terminal21-spark/src/test/scala/org/terminal21/sparklib/calculations/StdUiSparkCalculationTest.scala @@ -85,7 +85,7 @@ class StdUiSparkCalculationTest extends AnyFunSuiteLike with Eventually: given SparkSession = spark val calc = new TestingCalculation calc.run().get().collect().toList should be(List(2)) - val it = Controller(Seq(calc)).handledEventsIterator + val it = Controller(Seq(calc)).render().handledEventsIterator session.fireClickEvent(calc.recalc) session.fireSessionClosedEvent() it.lastOption diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index d3741fb5..711dc033 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -17,9 +17,9 @@ class Controller[M]( initialModel: Model[M], eventHandlers: Seq[PartialFunction[ControllerEvent[M], HandledEvent[M]]] = defaultEventHandlers[M] ): - def render()(using session: ConnectedSession): this.type = + def render()(using session: ConnectedSession): RenderedController[M] = session.render(initialComponents) - this + new RenderedController(eventIteratorFactory, initialModel, initialComponents, renderChanges, eventHandlers) def onEvent(handler: PartialFunction[ControllerEvent[M], HandledEvent[M]]) = new Controller( @@ -31,6 +31,13 @@ class Controller[M]( eventHandlers :+ handler ) +class RenderedController[M]( + eventIteratorFactory: => Iterator[CommandEvent], + initialModel: Model[M], + initialComponents: Seq[UiElement], + renderChanges: Seq[UiElement] => Unit, + eventHandlers: Seq[PartialFunction[ControllerEvent[M], HandledEvent[M]]] +): private def clickHandlersMap(h: HandledEvent[M]): Map[String, Seq[OnClickEventHandlerFunction[M]]] = h.componentsByKey.values .collect: @@ -48,13 +55,6 @@ class Controller[M]( (e.key, e.dataStore(initialModel.ChangeBooleanKey)) .toMap - private def initialComponentsByKeyMap: Map[String, UiElement] = - initialComponents - .flatMap(_.flat) - .map(c => (c.key, c)) - .toMap - .withDefault(key => throw new IllegalArgumentException(s"Component with key=$key is not available")) - private def updateComponentsFromEvent(handled: HandledEvent[M], event: CommandEvent): HandledEvent[M] = event match case _: ClientEvent => handled @@ -114,6 +114,13 @@ class Controller[M]( (handled.renderChanges.flatMap(_.flat) ++ handled.timedRenderChanges.flatMap(_.renderChanges).flatMap(_.flat)).map(e => (e.key, e)).toMap handled.copy(componentsByKey = handled.componentsByKey ++ newComponentsByKey) + private def initialComponentsByKeyMap: Map[String, UiElement] = + initialComponents + .flatMap(_.flat) + .map(c => (c.key, c)) + .toMap + .withDefault(key => throw new IllegalArgumentException(s"Component with key=$key is not available")) + def handledEventsIterator: EventIterator[HandledEvent[M]] = new EventIterator( eventIteratorFactory diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index c7a67cd7..991fd217 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -9,12 +9,13 @@ import org.terminal21.collections.SEList import org.terminal21.model.{ClientEvent, CommandEvent, OnChange, OnClick} class ControllerTest extends AnyFunSuiteLike: - val button = Button() - val buttonClick = OnClick(button.key) - val input = Input() - val inputChange = OnChange(input.key, "new-value") - val checkbox = Checkbox() - val checkBoxChange = OnChange(checkbox.key, "true") + val button = Button() + val buttonClick = OnClick(button.key) + val input = Input() + val inputChange = OnChange(input.key, "new-value") + val checkbox = Checkbox() + val checkBoxChange = OnChange(checkbox.key, "true") + given ConnectedSession = ConnectedSessionMock.newConnectedSessionMock def newController[M]( initialModel: Model[M], @@ -33,6 +34,7 @@ class ControllerTest extends AnyFunSuiteLike: newController(model, Seq(buttonClick), Seq(button)) .onEvent: event => if event.model > 1 then event.handled.terminate else event.handled.withModel(event.model + 1) + .render() .handledEventsIterator .map(_.model) .toList should be(List(0, 1)) @@ -43,6 +45,7 @@ class ControllerTest extends AnyFunSuiteLike: .onEvent: event => import event.* if event.model > 1 then handled.terminate else handled.withModel(event.model + 1) + .render() .handledEventsIterator .map(_.model) .toList should be(List(0, 1)) @@ -54,6 +57,7 @@ class ControllerTest extends AnyFunSuiteLike: case event: ControllerClickEvent[_] => import event.* handled.withModel(5) + .render() .handledEventsIterator .map(_.model) .toList should be(List(0, 0)) @@ -64,6 +68,7 @@ class ControllerTest extends AnyFunSuiteLike: .onEvent: event => import event.* if event.model > 1 then handled.terminate else handled.withModel(event.model + 1) + .render() .handledEventsIterator .map(_.model) .toList should be(List(0, 1)) @@ -75,6 +80,7 @@ class ControllerTest extends AnyFunSuiteLike: case event: ControllerClickEvent[_] => import event.* handled.withModel(5) + .render() .handledEventsIterator .map(_.model) .toList should be(List(0, 0)) @@ -88,6 +94,7 @@ class ControllerTest extends AnyFunSuiteLike: case ControllerClientEvent(handled, event: TestClientEvent) => import event.* handled.withModel(event.i).terminate + .render() .handledEventsIterator .map(_.model) .toList should be(List(0, 5)) @@ -98,6 +105,7 @@ class ControllerTest extends AnyFunSuiteLike: .onEvent: case ControllerClickEvent(`checkbox`, handled) => handled.withModel(5).terminate + .render() .handledEventsIterator .map(_.model) .toList should be(List(0, 0)) @@ -111,7 +119,8 @@ class ControllerTest extends AnyFunSuiteLike: button.onClick: event => event.handled.withModel(100).terminate ) - ).handledEventsIterator + ).render() + .handledEventsIterator .map(_.model) .toList should be(List(0, 100)) @@ -124,7 +133,8 @@ class ControllerTest extends AnyFunSuiteLike: input.onChange: event => event.handled.withModel(100).terminate ) - ).handledEventsIterator + ).render() + .handledEventsIterator .map(_.model) .toList should be(List(0, 100)) @@ -137,7 +147,8 @@ class ControllerTest extends AnyFunSuiteLike: checkbox.onChange: event => event.handled.withModel(100).terminate ) - ).handledEventsIterator + ).render() + .handledEventsIterator .map(_.model) .toList should be(List(0, 100)) @@ -146,6 +157,7 @@ class ControllerTest extends AnyFunSuiteLike: newController(model, Seq(buttonClick, buttonClick, buttonClick), Seq(button)) .onEvent: event => if event.model > 1 then event.handled.terminate.withModel(100) else event.handled.withModel(event.model + 1) + .render() .handledEventsIterator .map(_.model) .toList should be(List(0, 1, 2, 100)) @@ -157,6 +169,7 @@ class ControllerTest extends AnyFunSuiteLike: newController(Model(0), Seq(buttonClick), Seq(button), renderer) .onEvent: event => event.handled.withModel(event.model + 1).withRenderChanges(button.withText("changed")).terminate + .render() .handledEventsIterator .map(_.model) .toList should be(List(0, 1)) @@ -177,7 +190,7 @@ class ControllerTest extends AnyFunSuiteLike: checkbox ), renderer - ).handledEventsIterator.toList + ).render().handledEventsIterator.toList handled(1).renderChanges should be(List(button.withText("changed"))) handled(2).renderChanges should be(Nil) @@ -188,6 +201,7 @@ class ControllerTest extends AnyFunSuiteLike: newController(Model(0), Seq(buttonClick), Seq(button), renderer) .onEvent: event => event.handled.withModel(1).withTimedRenderChanges(TimedRenderChanges(10, button.withText("changed"))).terminate + .render() .handledEventsIterator .map(_.model) .toList should be(List(0, 1)) @@ -203,7 +217,7 @@ class ControllerTest extends AnyFunSuiteLike: button.onClick(using model): event => event.handled.withTimedRenderChanges(TimedRenderChanges(10, button.withText("changed"))).terminate ) - ).handledEventsIterator.toList(1).current(button) should be(button.withText("changed")) + ).render().handledEventsIterator.toList(1).current(button) should be(button.withText("changed")) test("timed changes event handlers are called"): val model = Model(0) @@ -216,7 +230,8 @@ class ControllerTest extends AnyFunSuiteLike: button.onClick(using model): event => event.handled.withTimedRenderChanges(TimedRenderChanges(10, c)) ) - ).handledEventsIterator + ).render() + .handledEventsIterator .map(_.model) .toList should be(List(0, 0, 2)) @@ -230,7 +245,8 @@ class ControllerTest extends AnyFunSuiteLike: import event.* handled.withModel(if input.current.value == "new-value" then 100 else -1).terminate ) - ).handledEventsIterator + ).render() + .handledEventsIterator .map(_.model) .toList should be(List(0, 100)) @@ -244,7 +260,8 @@ class ControllerTest extends AnyFunSuiteLike: import event.* handled.withModel(if checkbox.current.checked then 100 else -1).terminate ) - ).handledEventsIterator + ).render() + .handledEventsIterator .map(_.model) .toList should be(List(0, 100)) @@ -255,7 +272,7 @@ class ControllerTest extends AnyFunSuiteLike: event.handled.withRenderChanges(box.withChildren(button, checkbox)) ) - val handledEvents = newController(model, Seq(buttonClick), Seq(box)).handledEventsIterator.toList + val handledEvents = newController(model, Seq(buttonClick), Seq(box)).render().handledEventsIterator.toList handledEvents(1).componentsByKey(checkbox.key) should be(checkbox) test("newly rendered elements event handlers are invoked"): @@ -273,7 +290,9 @@ class ControllerTest extends AnyFunSuiteLike: lazy val box: Box = Box().withChildren(b) - newController(model, Seq(buttonClick, checkBoxChange), Seq(box)).handledEventsIterator + newController(model, Seq(buttonClick, checkBoxChange), Seq(box)) + .render() + .handledEventsIterator .map(_.model) .toList should be(List(0, 1, 2)) @@ -281,7 +300,9 @@ class ControllerTest extends AnyFunSuiteLike: var rendered = Seq.empty[UiElement] def renderer(s: Seq[UiElement]): Unit = rendered = s - newController(Model(0), Seq(RenderChangesEvent(Seq(button.withText("changed")))), Seq(button), renderer).handledEventsIterator + newController(Model(0), Seq(RenderChangesEvent(Seq(button.withText("changed")))), Seq(button), renderer) + .render() + .handledEventsIterator .map(_.model) .toList should be(List(0, 0)) rendered should be(Seq(button.withText("changed"))) From 8c6e4ac85c9708d520c6c34f521195d0e8631b3c Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 27 Feb 2024 19:18:09 +0000 Subject: [PATCH 195/313] - --- end-to-end-tests/src/main/scala/tests/LoginForm.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/LoginForm.scala b/end-to-end-tests/src/main/scala/tests/LoginForm.scala index 8db58a88..d1bda27d 100644 --- a/end-to-end-tests/src/main/scala/tests/LoginForm.scala +++ b/end-to-end-tests/src/main/scala/tests/LoginForm.scala @@ -35,10 +35,10 @@ class LoginForm(using session: ConnectedSession): .onClick: clickEvent => import clickEvent.* // if the email is invalid, we will not terminate. We also will render an error that will be visible for 2 seconds - val isValidEmail = clickEvent.model.isValidEmail + val isValidEmail = model.isValidEmail val messageBox = if isValidEmail then errorsBox.current else errorsBox.current.addChildren(errorMsgInvalidEmail) - clickEvent.handled.withShouldTerminate(isValidEmail).withRenderChanges(messageBox).addTimedRenderChange(2000, errorsBox) + handled.withShouldTerminate(isValidEmail).withRenderChanges(messageBox).addTimedRenderChange(2000, errorsBox) val passwordInput = Input(`type` = "password", defaultValue = initialModel.value.pwd) val errorsBox = Box() From c53f42dae37d2ddbbff897e8126645fff970d273 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 27 Feb 2024 19:19:35 +0000 Subject: [PATCH 196/313] - --- .../src/main/scala/org/terminal21/client/Controller.scala | 6 ++---- .../test/scala/org/terminal21/client/ControllerTest.scala | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 711dc033..7f38c9c8 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -11,7 +11,6 @@ import org.terminal21.model.{ClientEvent, CommandEvent, OnChange, OnClick} class Controller[M]( eventIteratorFactory: => Iterator[CommandEvent], - fireEvent: CommandEvent => Unit, renderChanges: Seq[UiElement] => Unit, initialComponents: Seq[UiElement], initialModel: Model[M], @@ -24,7 +23,6 @@ class Controller[M]( def onEvent(handler: PartialFunction[ControllerEvent[M], HandledEvent[M]]) = new Controller( eventIteratorFactory, - fireEvent, renderChanges, initialComponents, initialModel, @@ -150,9 +148,9 @@ object Controller: private def defaultEventHandlers[M] = Seq(renderChangesEventHandler[M]) def apply[M](initialModel: Model[M], components: Seq[UiElement])(using session: ConnectedSession): Controller[M] = - new Controller(session.eventIterator, session.fireEvent, session.renderChanges, components, initialModel) + new Controller(session.eventIterator, session.renderChanges, components, initialModel) def apply[M](components: Seq[UiElement])(using initialModel: Model[M], session: ConnectedSession): Controller[M] = - new Controller(session.eventIterator, session.fireEvent, session.renderChanges, components, initialModel) + new Controller(session.eventIterator, session.renderChanges, components, initialModel) def apply[M](component: UiElement)(using initialModel: Model[M], session: ConnectedSession): Controller[M] = apply(Seq(component)) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 991fd217..36bd93ab 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -27,7 +27,7 @@ class ControllerTest extends AnyFunSuiteLike: val it = seList.iterator events.foreach(e => seList.add(e)) seList.add(CommandEvent.sessionClosed) - new Controller(it, _ => (), renderChanges, components, initialModel) + new Controller(it, renderChanges, components, initialModel) test("onEvent is called"): val model = Model(0) From 7bfcf3551a9b84aafc20e64ee7bfe19fa6939667 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 27 Feb 2024 21:16:33 +0000 Subject: [PATCH 197/313] - --- .../serverapp/bundled/AppManager.scala | 7 +- .../serverapp/bundled/ServerStatusApp.scala | 53 ++--- .../serverapp/bundled/SettingsApp.scala | 2 +- .../bundled/AppManagerPageTest.scala | 123 ++++++------ .../bundled/ServerStatusPageTest.scala | 158 +++++++-------- .../serverapp/bundled/SettingsPageTest.scala | 28 +-- .../org/terminal21/client/Controller.scala | 105 +++------- .../client/components/EventHandler.scala | 6 +- .../client/components/StdUiCalculation.scala | 158 +++++++-------- .../client/components/UiElement.scala | 6 +- .../components/chakra/ChakraElement.scala | 8 - .../client/components/std/StdElement.scala | 1 - .../client/components/std/StdHttp.scala | 2 - .../terminal21/client/ControllerTest.scala | 189 +++--------------- 14 files changed, 336 insertions(+), 510 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala index c8e3b751..cd02ae0f 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala @@ -39,7 +39,7 @@ class AppManagerPage(apps: Seq[ServerSideApp], startApp: ServerSideApp => Unit)( Text(text = app.description) ) - def components = + def components(m: ManagerModel): Seq[UiElement] = val appsTable = QuickTable( caption = Some("Apps installed on the server, click one to run it."), rows = appRows @@ -63,16 +63,13 @@ class AppManagerPage(apps: Seq[ServerSideApp], startApp: ServerSideApp => Unit)( ) ) - def controller(components: Seq[UiElement]): Controller[ManagerModel] = + def controller: Controller[ManagerModel] = Controller(components) .onEvent: event => import event.* // for every event, reset the startApp so that it doesn't start the same app on each event handled.withModel(model.copy(startApp = None)) - def controller: Controller[ManagerModel] = - controller(components) - def eventsIterator: Iterator[ManagerModel] = controller .render() diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 0b0a5b72..585653c2 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -24,7 +24,9 @@ class ServerStatusPage( serverSideSessions: ServerSideSessions, sessionsService: ServerSessionsService )(using appSession: ConnectedSession, fiberExecutor: FiberExecutor): - import Model.Standard.unitModel + case class StatusModel(runtime: Runtime, sessions: Seq[Session]) + val initModel = StatusModel(Runtime.getRuntime, sessionsService.allSessions) + given Model[StatusModel] = Model(initModel) case object Ticker extends ClientEvent @@ -33,48 +35,53 @@ class ServerStatusPage( while !appSession.isClosed do Thread.sleep(2000) appSession.fireEvents(Ticker) - controller(Runtime.getRuntime, sessionsService.allSessions).render().handledEventsIterator.lastOption + + try controller.render().handledEventsIterator.lastOption + catch case t: Throwable => t.printStackTrace() private def toMb(v: Long) = s"${v / (1024 * 1024)} MB" private val xs = Some("2xs") - def controller(runtime: Runtime, sessions: => Seq[Session]): Controller[Unit] = - Controller(components(runtime, sessions)).onEvent: + def controller: Controller[StatusModel] = + Controller(components).onEvent: case ControllerClientEvent(handled, Ticker) => - handled.withRenderChanges(sessionsTable(sessions)) + handled.withModel(handled.model.copy(sessions = sessionsService.allSessions)) - def components(runtime: Runtime, sessions: Seq[Session]): Seq[UiElement] = - Seq(jvmTable(runtime), sessionsTable(sessions)) + def components(m: StatusModel): Seq[UiElement] = + Seq(jvmTable(m.runtime), sessionsTable(m.sessions)) - def jvmTable(runtime: Runtime) = QuickTable(key = "jvmTable", caption = Some("JVM")) + private val jvmTableE = QuickTable(key = "jvmTable", caption = Some("JVM")) .withHeaders("Property", "Value", "Actions") - .withRows( + private val gcButton = Button(size = xs, text = "Run GC") + .onClick: event => + System.gc() + event.handled + + def jvmTable(runtime: Runtime) = + jvmTableE.withRows( Seq( Seq("Free Memory", toMb(runtime.freeMemory()), ""), Seq("Max Memory", toMb(runtime.maxMemory()), ""), - Seq( - "Total Memory", - toMb(runtime.totalMemory()), - Button(size = xs, text = "Run GC").onClick: event => - System.gc() - event.handled - ), + Seq("Total Memory", toMb(runtime.totalMemory()), gcButton), Seq("Available processors", runtime.availableProcessors(), "") ) ) - def sessionsTable(sessions: Seq[Session]) = QuickTable( - key = "sessions-table", - caption = Some("All sessions"), - rows = sessions.map: session => + private val sessionsTableE = + QuickTable( + key = "sessions-table", + caption = Some("All sessions") + ).withHeaders("Id", "Name", "Is Open", "Actions") + + def sessionsTable(sessions: Seq[Session]) = sessionsTableE.withRows( + sessions.map: session => Seq(Text(text = session.id), Text(text = session.name), if session.isOpen then CheckIcon() else NotAllowedIcon(), actionsFor(session)) ) - .withHeaders("Id", "Name", "Is Open", "Actions") private def actionsFor(session: Session): UiElement = if session.isOpen then Box().withChildren( - Button(text = "Close", size = xs) + Button(key = s"close-${session.id}", text = "Close", size = xs) .withLeftIcon(SmallCloseIcon()) .onClick: event => import event.* @@ -82,7 +89,7 @@ class ServerStatusPage( handled , Text(text = " "), - Button(text = "View State", size = xs) + Button(key = s"view-${session.id}", text = "View State", size = xs) .withLeftIcon(ChatIcon()) .onClick: event => serverSideSessions diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala index 370870f3..8d102019 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala @@ -24,6 +24,6 @@ class SettingsPage(using session: ConnectedSession): def run() = controller.render().handledEventsIterator.lastOption - def components = Seq(themeToggle) + def components(u: Unit) = Seq(themeToggle) def controller = Controller(components) diff --git a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala index 5eb11da2..c72dfbf1 100644 --- a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala +++ b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala @@ -1,62 +1,61 @@ -package org.terminal21.serverapp.bundled - -import org.mockito.Mockito -import org.mockito.Mockito.when -import org.scalatest.funsuite.AnyFunSuiteLike -import org.scalatestplus.mockito.MockitoSugar.mock -import org.terminal21.client.components.* -import org.terminal21.client.components.chakra.{Link, Text} -import org.terminal21.client.{ConnectedSession, ConnectedSessionMock} -import org.terminal21.serverapp.ServerSideApp -import org.scalatest.matchers.should.Matchers.* -import org.terminal21.model.CommandEvent - -class AppManagerPageTest extends AnyFunSuiteLike: - def mockApp(name: String, description: String) = - val app = mock[ServerSideApp] - when(app.name).thenReturn(name) - when(app.description).thenReturn(description) - app - - class App(apps: ServerSideApp*): - given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock - var startedApp: Option[ServerSideApp] = None - val page = new AppManagerPage(apps, app => startedApp = Some(app)) - def allComponents = page.components.flatMap(_.flat) - - test("renders app links"): - new App(mockApp("app1", "the-app1-desc")): - allComponents - .collect: - case l: Link if l.text == "app1" => l - .size should be(1) - - test("renders app description"): - new App(mockApp("app1", "the-app1-desc")): - allComponents - .collect: - case t: Text if t.text == "the-app1-desc" => t - .size should be(1) - - test("renders the discussions link"): - new App(): - allComponents - .collect: - case l: Link if l.href == "https://github.com/kostaskougios/terminal21-restapi/discussions" => l - .size should be(1) - - test("starts app when app link is clicked"): - val app = mockApp("app1", "the-app1-desc") - new App(app): - val eventsIt = page.eventsIterator - session.fireEvents(CommandEvent.onClick(page.appRows.head.head), CommandEvent.sessionClosed) - eventsIt.toList - startedApp should be(Some(app)) - - test("resets startApp state on other events"): - val app = mockApp("app1", "the-app1-desc") - new App(app): - val other = Link() - val eventsIt = page.controller(page.components :+ other).render().handledEventsIterator - session.fireEvents(CommandEvent.onClick(page.appRows.head.head), CommandEvent.onClick(other), CommandEvent.sessionClosed) - eventsIt.toList.map(_.model).last.startApp should be(None) +//package org.terminal21.serverapp.bundled +// +//import org.mockito.Mockito +//import org.mockito.Mockito.when +//import org.scalatest.funsuite.AnyFunSuiteLike +//import org.scalatestplus.mockito.MockitoSugar.mock +//import org.terminal21.client.components.* +//import org.terminal21.client.components.chakra.{Link, Text} +//import org.terminal21.client.{ConnectedSession, ConnectedSessionMock} +//import org.terminal21.serverapp.ServerSideApp +//import org.scalatest.matchers.should.Matchers.* +//import org.terminal21.model.CommandEvent +// +//class AppManagerPageTest extends AnyFunSuiteLike: +// def mockApp(name: String, description: String) = +// val app = mock[ServerSideApp] +// when(app.name).thenReturn(name) +// when(app.description).thenReturn(description) +// app +// +// class App(apps: ServerSideApp*): +// given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock +// var startedApp: Option[ServerSideApp] = None +// val page = new AppManagerPage(apps, app => startedApp = Some(app)) +// +// test("renders app links"): +// new App(mockApp("app1", "the-app1-desc")): +// allComponents +// .collect: +// case l: Link if l.text == "app1" => l +// .size should be(1) +// +// test("renders app description"): +// new App(mockApp("app1", "the-app1-desc")): +// allComponents +// .collect: +// case t: Text if t.text == "the-app1-desc" => t +// .size should be(1) +// +// test("renders the discussions link"): +// new App(): +// allComponents +// .collect: +// case l: Link if l.href == "https://github.com/kostaskougios/terminal21-restapi/discussions" => l +// .size should be(1) +// +// test("starts app when app link is clicked"): +// val app = mockApp("app1", "the-app1-desc") +// new App(app): +// val eventsIt = page.eventsIterator +// session.fireEvents(CommandEvent.onClick(page.appRows.head.head), CommandEvent.sessionClosed) +// eventsIt.toList +// startedApp should be(Some(app)) +// +// test("resets startApp state on other events"): +// val app = mockApp("app1", "the-app1-desc") +// new App(app): +// val other = Link() +// val eventsIt = page.controller(page.components :+ other).render().handledEventsIterator +// session.fireEvents(CommandEvent.onClick(page.appRows.head.head), CommandEvent.onClick(other), CommandEvent.sessionClosed) +// eventsIt.toList.map(_.model).last.startApp should be(None) diff --git a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala index 417fd896..d7ea3c5e 100644 --- a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala +++ b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala @@ -1,79 +1,79 @@ -package org.terminal21.serverapp.bundled - -import org.scalatest.funsuite.AnyFunSuiteLike -import org.scalatestplus.mockito.MockitoSugar.mock -import org.terminal21.client.components.chakra.{Button, CheckIcon, NotAllowedIcon, Text} -import org.terminal21.client.{ConnectedSession, ConnectedSessionMock} -import org.terminal21.model.CommonModelBuilders.session -import org.terminal21.model.{CommandEvent, CommonModelBuilders, Session} -import org.terminal21.server.service.ServerSessionsService -import org.terminal21.serverapp.ServerSideSessions -import org.terminal21.client.given -import org.scalatest.matchers.should.Matchers.* - -class ServerStatusPageTest extends AnyFunSuiteLike: - class App: - given connectedSession: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock - val sessionsService = mock[ServerSessionsService] - val serverSideSessions = mock[ServerSideSessions] - val page = new ServerStatusPage(serverSideSessions, sessionsService) - - test("Close button for a session"): - new App: - page - .sessionsTable(Seq(session())) - .flat - .collectFirst: - case b: Button if b.text == "Close" => b - .isEmpty should be(false) - - test("View state button for a session"): - new App: - page - .sessionsTable(Seq(session())) - .flat - .collectFirst: - case b: Button if b.text == "View State" => b - .isEmpty should be(false) - - test("When session is open, a CheckIcon is displayed"): - new App: - page - .sessionsTable(Seq(session())) - .flat - .collectFirst: - case i: CheckIcon => i - .isEmpty should be(false) - - test("When session is closed, a NotAllowedIcon is displayed"): - new App: - page - .sessionsTable(Seq(session(isOpen = false))) - .flat - .collectFirst: - case i: NotAllowedIcon => i - .isEmpty should be(false) - - test("sessions are rendered when Ticker event is fired"): - new App: - var times = 0 - def sessions = - times += 1 - times match - case 1 => Seq(session(id = "s1", name = "session 1")) // this is initially rendered - case 2 => Seq(session(id = "s2", name = "session 2")) // this is a change - case 3 => Seq(session(id = "s3", name = "session 3")) // this is also a change - val it = page.controller(Runtime.getRuntime, sessions).render().handledEventsIterator - connectedSession.fireEvents(page.Ticker, page.Ticker, CommandEvent.sessionClosed) - val handledEvents = it.toList - handledEvents.head.renderChanges should be(Nil) - handledEvents(1).renderChanges - .flatMap(_.flat) - .collectFirst: - case t: Text if t.text == "session 2" => t - .size should be(1) - handledEvents(2).renderChanges - .flatMap(_.flat) - .collectFirst: - case t: Text if t.text == "session 3" => t - .size should be(1) +//package org.terminal21.serverapp.bundled +// +//import org.scalatest.funsuite.AnyFunSuiteLike +//import org.scalatestplus.mockito.MockitoSugar.mock +//import org.terminal21.client.components.chakra.{Button, CheckIcon, NotAllowedIcon, Text} +//import org.terminal21.client.{ConnectedSession, ConnectedSessionMock} +//import org.terminal21.model.CommonModelBuilders.session +//import org.terminal21.model.{CommandEvent, CommonModelBuilders, Session} +//import org.terminal21.server.service.ServerSessionsService +//import org.terminal21.serverapp.ServerSideSessions +//import org.terminal21.client.given +//import org.scalatest.matchers.should.Matchers.* +// +//class ServerStatusPageTest extends AnyFunSuiteLike: +// class App: +// given connectedSession: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock +// val sessionsService = mock[ServerSessionsService] +// val serverSideSessions = mock[ServerSideSessions] +// val page = new ServerStatusPage(serverSideSessions, sessionsService) +// +// test("Close button for a session"): +// new App: +// page +// .sessionsTable(Seq(session())) +// .flat +// .collectFirst: +// case b: Button if b.text == "Close" => b +// .isEmpty should be(false) +// +// test("View state button for a session"): +// new App: +// page +// .sessionsTable(Seq(session())) +// .flat +// .collectFirst: +// case b: Button if b.text == "View State" => b +// .isEmpty should be(false) +// +// test("When session is open, a CheckIcon is displayed"): +// new App: +// page +// .sessionsTable(Seq(session())) +// .flat +// .collectFirst: +// case i: CheckIcon => i +// .isEmpty should be(false) +// +// test("When session is closed, a NotAllowedIcon is displayed"): +// new App: +// page +// .sessionsTable(Seq(session(isOpen = false))) +// .flat +// .collectFirst: +// case i: NotAllowedIcon => i +// .isEmpty should be(false) +// +// test("sessions are rendered when Ticker event is fired"): +// new App: +// var times = 0 +// def sessions = +// times += 1 +// times match +// case 1 => Seq(session(id = "s1", name = "session 1")) // this is initially rendered +// case 2 => Seq(session(id = "s2", name = "session 2")) // this is a change +// case 3 => Seq(session(id = "s3", name = "session 3")) // this is also a change +// val it = page.controller(Runtime.getRuntime, sessions).render().handledEventsIterator +// connectedSession.fireEvents(page.Ticker, page.Ticker, CommandEvent.sessionClosed) +// val handledEvents = it.toList +// handledEvents.head.renderChanges should be(Nil) +// handledEvents(1).renderChanges +// .flatMap(_.flat) +// .collectFirst: +// case t: Text if t.text == "session 2" => t +// .size should be(1) +// handledEvents(2).renderChanges +// .flatMap(_.flat) +// .collectFirst: +// case t: Text if t.text == "session 3" => t +// .size should be(1) diff --git a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/SettingsPageTest.scala b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/SettingsPageTest.scala index 5a88f916..54910226 100644 --- a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/SettingsPageTest.scala +++ b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/SettingsPageTest.scala @@ -1,14 +1,14 @@ -package org.terminal21.serverapp.bundled - -import org.scalatest.funsuite.AnyFunSuiteLike -import org.scalatest.matchers.should.Matchers.* -import org.terminal21.client.{*, given} - -class SettingsPageTest extends AnyFunSuiteLike: - class App: - given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock - val page = new SettingsPage - - test("Should render the ThemeToggle component"): - new App: - page.components should contain(page.themeToggle) +//package org.terminal21.serverapp.bundled +// +//import org.scalatest.funsuite.AnyFunSuiteLike +//import org.scalatest.matchers.should.Matchers.* +//import org.terminal21.client.{*, given} +// +//class SettingsPageTest extends AnyFunSuiteLike: +// class App: +// given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock +// val page = new SettingsPage +// +// test("Should render the ThemeToggle component"): +// new App: +// page.components should contain(page.themeToggle) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 7f38c9c8..428fb5ff 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -1,10 +1,9 @@ package org.terminal21.client -import org.terminal21.client.Controller.defaultEventHandlers +import org.slf4j.LoggerFactory import org.terminal21.client.collections.EventIterator import org.terminal21.client.components.OnChangeEventHandler.CanHandleOnChangeEvent import org.terminal21.client.components.OnClickEventHandler.CanHandleOnClickEvent -import org.terminal21.client.components.UiElement.HasEventHandler import org.terminal21.client.components.{OnChangeBooleanEventHandler, OnChangeEventHandler, OnClickEventHandler, UiElement} import org.terminal21.collections.TypedMapKey import org.terminal21.model.{ClientEvent, CommandEvent, OnChange, OnClick} @@ -12,19 +11,19 @@ import org.terminal21.model.{ClientEvent, CommandEvent, OnChange, OnClick} class Controller[M]( eventIteratorFactory: => Iterator[CommandEvent], renderChanges: Seq[UiElement] => Unit, - initialComponents: Seq[UiElement], + modelComponents: M => Seq[UiElement], initialModel: Model[M], - eventHandlers: Seq[PartialFunction[ControllerEvent[M], HandledEvent[M]]] = defaultEventHandlers[M] + eventHandlers: Seq[PartialFunction[ControllerEvent[M], HandledEvent[M]]] ): def render()(using session: ConnectedSession): RenderedController[M] = - session.render(initialComponents) - new RenderedController(eventIteratorFactory, initialModel, initialComponents, renderChanges, eventHandlers) + session.render(modelComponents(initialModel.value)) + new RenderedController(eventIteratorFactory, initialModel, modelComponents, renderChanges, eventHandlers) def onEvent(handler: PartialFunction[ControllerEvent[M], HandledEvent[M]]) = new Controller( eventIteratorFactory, renderChanges, - initialComponents, + modelComponents, initialModel, eventHandlers :+ handler ) @@ -32,10 +31,11 @@ class Controller[M]( class RenderedController[M]( eventIteratorFactory: => Iterator[CommandEvent], initialModel: Model[M], - initialComponents: Seq[UiElement], + modelComponents: M => Seq[UiElement], renderChanges: Seq[UiElement] => Unit, eventHandlers: Seq[PartialFunction[ControllerEvent[M], HandledEvent[M]]] ): + private val logger = LoggerFactory.getLogger(getClass) private def clickHandlersMap(h: HandledEvent[M]): Map[String, Seq[OnClickEventHandlerFunction[M]]] = h.componentsByKey.values .collect: @@ -53,20 +53,8 @@ class RenderedController[M]( (e.key, e.dataStore(initialModel.ChangeBooleanKey)) .toMap - private def updateComponentsFromEvent(handled: HandledEvent[M], event: CommandEvent): HandledEvent[M] = - event match - case _: ClientEvent => handled - case _ => - handled.componentsByKey(event.key) match - case e: UiElement with HasEventHandler => - event match - case OnChange(key, value) => - handled.copy(componentsByKey = handled.componentsByKey + (key -> e.defaultEventHandler(value))) - case _ => handled - case _ => handled - private def invokeEventHandlers(initHandled: HandledEvent[M], event: CommandEvent): HandledEvent[M] = - eventHandlers.foldLeft(initHandled.copy(renderChanges = Nil, timedRenderChanges = Nil)): (h, f) => + eventHandlers.foldLeft(initHandled): (h, f) => event match case OnClick(key) => val e = ControllerClickEvent(h.componentsByKey(key), h) @@ -107,33 +95,30 @@ class RenderedController[M]( handled case _ => h - private def includeRendered(handled: HandledEvent[M]): HandledEvent[M] = - val newComponentsByKey = - (handled.renderChanges.flatMap(_.flat) ++ handled.timedRenderChanges.flatMap(_.renderChanges).flatMap(_.flat)).map(e => (e.key, e)).toMap - handled.copy(componentsByKey = handled.componentsByKey ++ newComponentsByKey) - private def initialComponentsByKeyMap: Map[String, UiElement] = - initialComponents + val all = modelComponents(initialModel.value) .flatMap(_.flat) .map(c => (c.key, c)) .toMap - .withDefault(key => throw new IllegalArgumentException(s"Component with key=$key is not available")) + all.withDefault(key => + throw new IllegalArgumentException( + s"Component with key=$key is not available. Here are all available components:\n${all.values.map(_.toSimpleString).mkString("\n")}" + ) + ) def handledEventsIterator: EventIterator[HandledEvent[M]] = new EventIterator( eventIteratorFactory .takeWhile(!_.isSessionClosed) - .scanLeft(HandledEvent(initialModel.value, initialComponentsByKeyMap, Nil, Nil, false)): (oldHandled, event) => - val handled1 = includeRendered(updateComponentsFromEvent(oldHandled, event)) - val handled2 = includeRendered(invokeEventHandlers(handled1, event)) - val handled3 = includeRendered(invokeComponentEventHandlers(handled2, event)) - handled3 - .tapEach: handled => - renderChanges(handled.renderChanges) - for trc <- handled.timedRenderChanges do - fiberExecutor.submit: - Thread.sleep(trc.waitInMs) - renderChanges(trc.renderChanges) + .scanLeft(HandledEvent(initialModel.value, initialComponentsByKeyMap, false)): (oldHandled, event) => + try + val handled2 = invokeEventHandlers(oldHandled, event) + val handled3 = invokeComponentEventHandlers(handled2, event) + handled3 + catch + case t: Throwable => + logger.error("an error occurred while iterating events", t) + oldHandled .flatMap: h => // trick to make sure we take the last state of the model when shouldTerminate=true if h.shouldTerminate then Seq(h.copy(shouldTerminate = false), h) else Seq(h) @@ -141,23 +126,14 @@ class RenderedController[M]( ) object Controller: - private def renderChangesEventHandler[M]: PartialFunction[ControllerEvent[M], HandledEvent[M]] = - case ControllerClientEvent(handled, RenderChangesEvent(changes)) => - handled.withRenderChanges(changes*) - - private def defaultEventHandlers[M] = Seq(renderChangesEventHandler[M]) - - def apply[M](initialModel: Model[M], components: Seq[UiElement])(using session: ConnectedSession): Controller[M] = - new Controller(session.eventIterator, session.renderChanges, components, initialModel) - def apply[M](components: Seq[UiElement])(using initialModel: Model[M], session: ConnectedSession): Controller[M] = - new Controller(session.eventIterator, session.renderChanges, components, initialModel) - def apply[M](component: UiElement)(using initialModel: Model[M], session: ConnectedSession): Controller[M] = - apply(Seq(component)) + def apply[M](initialModel: Model[M], modelComponents: M => Seq[UiElement])(using session: ConnectedSession): Controller[M] = + new Controller(session.eventIterator, session.renderChanges, modelComponents, initialModel, Nil) + def apply[M](modelComponents: M => Seq[UiElement])(using initialModel: Model[M], session: ConnectedSession): Controller[M] = + new Controller(session.eventIterator, session.renderChanges, modelComponents, initialModel, Nil) sealed trait ControllerEvent[M]: - def model: M = handled.model + def model: M = handled.model def handled: HandledEvent[M] - extension [A <: UiElement](e: A) def current: A = handled.current(e) case class ControllerClickEvent[M](clicked: UiElement, handled: HandledEvent[M]) extends ControllerEvent[M] case class ControllerChangeEvent[M](changed: UiElement, handled: HandledEvent[M], newValue: String) extends ControllerEvent[M] @@ -167,27 +143,16 @@ case class ControllerClientEvent[M](handled: HandledEvent[M], event: ClientEvent case class HandledEvent[M]( model: M, componentsByKey: Map[String, UiElement], - renderChanges: Seq[UiElement], - timedRenderChanges: Seq[TimedRenderChanges], shouldTerminate: Boolean ): - def terminate: HandledEvent[M] = copy(shouldTerminate = true) - def withShouldTerminate(t: Boolean): HandledEvent[M] = copy(shouldTerminate = t) - def withModel(m: M): HandledEvent[M] = copy(model = m) - def withRenderChanges(changed: UiElement*): HandledEvent[M] = copy(renderChanges = renderChanges ++ changed) - def withTimedRenderChanges(changed: TimedRenderChanges*): HandledEvent[M] = copy(timedRenderChanges = changed) - def addTimedRenderChange(waitInMs: Long, renderChanges: UiElement): HandledEvent[M] = - copy(timedRenderChanges = timedRenderChanges :+ TimedRenderChanges(waitInMs, renderChanges)) - def current[A <: UiElement](e: A): A = componentsByKey(e.key).asInstanceOf[A] + def terminate: HandledEvent[M] = copy(shouldTerminate = true) + def withShouldTerminate(t: Boolean): HandledEvent[M] = copy(shouldTerminate = t) + def withModel(m: M): HandledEvent[M] = copy(model = m) type OnClickEventHandlerFunction[M] = ControllerClickEvent[M] => HandledEvent[M] type OnChangeEventHandlerFunction[M] = ControllerChangeEvent[M] => HandledEvent[M] type OnChangeBooleanEventHandlerFunction[M] = ControllerChangeBooleanEvent[M] => HandledEvent[M] -case class TimedRenderChanges(waitInMs: Long, renderChanges: Seq[UiElement]) -object TimedRenderChanges: - def apply(waitInMs: Long, renderChanges: UiElement): TimedRenderChanges = TimedRenderChanges(waitInMs, Seq(renderChanges)) - case class Model[M](value: M): object ClickKey extends TypedMapKey[Seq[OnClickEventHandlerFunction[M]]] object ChangeKey extends TypedMapKey[Seq[OnChangeEventHandlerFunction[M]]] @@ -198,9 +163,3 @@ object Model: given unitModel: Model[Unit] = Model(()) given booleanFalseModel: Model[Boolean] = Model(false) given booleanTrueModel: Model[Boolean] = Model(true) - -/** Used to render changes outside the controller iteration - * @param changes - * the changes to be rendered - */ -case class RenderChangesEvent(changes: Seq[UiElement]) extends ClientEvent diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala index 9c3aef3e..c983b270 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala @@ -1,6 +1,6 @@ package org.terminal21.client.components -import org.terminal21.client.components.UiElement.{HasDataStore, HasEventHandler} +import org.terminal21.client.components.UiElement.HasDataStore import org.terminal21.client.{Model, OnChangeBooleanEventHandlerFunction, OnChangeEventHandlerFunction, OnClickEventHandlerFunction} trait EventHandler @@ -13,14 +13,14 @@ object OnClickEventHandler: store(model.ClickKey, handlers :+ h) object OnChangeEventHandler: - trait CanHandleOnChangeEvent extends HasDataStore with HasEventHandler: + trait CanHandleOnChangeEvent extends HasDataStore: this: UiElement => def onChange[M](using model: Model[M])(h: OnChangeEventHandlerFunction[M]): This = val handlers = dataStore.getOrElse(model.ChangeKey, Nil) store(model.ChangeKey, handlers :+ h) object OnChangeBooleanEventHandler: - trait CanHandleOnChangeEvent extends HasDataStore with HasEventHandler: + trait CanHandleOnChangeEvent extends HasDataStore: this: UiElement => def onChange[M](using model: Model[M])(h: OnChangeBooleanEventHandlerFunction[M]): This = val handlers = dataStore.getOrElse(model.ChangeBooleanKey, Nil) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala index 551316e0..1142af0c 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala @@ -1,79 +1,79 @@ -package org.terminal21.client.components - -import functions.fibers.FiberExecutor -import org.terminal21.client.{ConnectedSession, Model, RenderChangesEvent} -import org.terminal21.client.components.UiElement.HasStyle -import org.terminal21.client.components.chakra.* - -import java.util.concurrent.atomic.{AtomicBoolean, AtomicReference} - -/** Creates a standard UI for a calculation which may take time. While the calculation runs, the UI is grayed out, including the dataUi component. When the - * calculation completes, it allows for updating the dataUi component. - * @tparam OUT - * the return value of the calculation. - */ -trait StdUiCalculation[OUT]( - name: String, - dataUi: UiElement with HasStyle -)(using session: ConnectedSession, model: Model[_], executor: FiberExecutor) - extends Calculation[OUT] - with UiComponent: - private val running = new AtomicBoolean(false) - private val currentUi = new AtomicReference(dataUi) - - protected def updateUi(dataUi: UiElement & HasStyle) = currentUi.set(dataUi) - - lazy val badge = Badge() - lazy val recalc = Button(text = "Recalculate", size = Some("sm"), leftIcon = Some(RepeatIcon())).onClick: event => - import event.* - if running.compareAndSet(false, true) then - try - reCalculate() - finally running.set(false) - handled - - override lazy val rendered: Seq[UiElement] = - val header = Box( - bg = "green", - p = 4, - children = Seq( - HStack(children = Seq(Text(text = name), badge, recalc)) - ) - ) - Seq(header, dataUi) - - override def onError(t: Throwable): Unit = - session.fireEvent( - RenderChangesEvent( - Seq( - badge.withText(s"Error: ${t.getMessage}").withColorScheme(Some("red")), - dataUi, - recalc.withIsDisabled(None) - ) - ) - ) - super.onError(t) - - override protected def whenResultsNotReady(): Unit = - session.fireEvent( - RenderChangesEvent( - Seq( - badge.withText("Calculating").withColorScheme(Some("purple")), - currentUi.get().withStyle(dataUi.style + ("filter" -> "grayscale(100%)")), - recalc.withIsDisabled(Some(true)) - ) - ) - ) - super.whenResultsNotReady() - - override protected def whenResultsReady(results: OUT): Unit = - val newDataUi = currentUi.get().withStyle(dataUi.style - "filter") - session.fireEvent( - RenderChangesEvent( - Seq( - badge.withText("Ready").withColorScheme(None), - newDataUi, - recalc.withIsDisabled(Some(false)) - ) - ) - ) +//package org.terminal21.client.components +// +//import functions.fibers.FiberExecutor +//import org.terminal21.client.{ConnectedSession, Model} +//import org.terminal21.client.components.UiElement.HasStyle +//import org.terminal21.client.components.chakra.* +// +//import java.util.concurrent.atomic.{AtomicBoolean, AtomicReference} +// +///** Creates a standard UI for a calculation which may take time. While the calculation runs, the UI is grayed out, including the dataUi component. When the +// * calculation completes, it allows for updating the dataUi component. +// * @tparam OUT +// * the return value of the calculation. +// */ +//trait StdUiCalculation[OUT]( +// name: String, +// dataUi: UiElement with HasStyle +//)(using session: ConnectedSession, model: Model[_], executor: FiberExecutor) +// extends Calculation[OUT] +// with UiComponent: +// private val running = new AtomicBoolean(false) +// private val currentUi = new AtomicReference(dataUi) +// +// protected def updateUi(dataUi: UiElement & HasStyle) = currentUi.set(dataUi) +// +// lazy val badge = Badge() +// lazy val recalc = Button(text = "Recalculate", size = Some("sm"), leftIcon = Some(RepeatIcon())).onClick: event => +// import event.* +// if running.compareAndSet(false, true) then +// try +// reCalculate() +// finally running.set(false) +// handled +// +// override lazy val rendered: Seq[UiElement] = +// val header = Box( +// bg = "green", +// p = 4, +// children = Seq( +// HStack(children = Seq(Text(text = name), badge, recalc)) +// ) +// ) +// Seq(header, dataUi) +// +// override def onError(t: Throwable): Unit = +// session.fireEvent( +// RenderChangesEvent( +// Seq( +// badge.withText(s"Error: ${t.getMessage}").withColorScheme(Some("red")), +// dataUi, +// recalc.withIsDisabled(None) +// ) +// ) +// ) +// super.onError(t) +// +// override protected def whenResultsNotReady(): Unit = +// session.fireEvent( +// RenderChangesEvent( +// Seq( +// badge.withText("Calculating").withColorScheme(Some("purple")), +// currentUi.get().withStyle(dataUi.style + ("filter" -> "grayscale(100%)")), +// recalc.withIsDisabled(Some(true)) +// ) +// ) +// ) +// super.whenResultsNotReady() +// +// override protected def whenResultsReady(results: OUT): Unit = +// val newDataUi = currentUi.get().withStyle(dataUi.style - "filter") +// session.fireEvent( +// RenderChangesEvent( +// Seq( +// badge.withText("Ready").withColorScheme(None), +// newDataUi, +// recalc.withIsDisabled(Some(false)) +// ) +// ) +// ) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala index 7e6beea2..65b5dfb0 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala @@ -12,6 +12,8 @@ trait UiElement extends AnyElement: */ def flat: Seq[UiElement] = Seq(this) + def toSimpleString: String = s"${getClass.getSimpleName}($key)" + object UiElement: trait HasChildren: this: UiElement => @@ -21,10 +23,6 @@ object UiElement: def noChildren: This = withChildren() def addChildren(cn: UiElement*): This = withChildren(children ++ cn: _*) - trait HasEventHandler: - this: UiElement => - def defaultEventHandler: String => This - trait HasStyle: this: UiElement => def style: Map[String, Any] diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala index 8f4d2fd9..0f54b002 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala @@ -165,8 +165,6 @@ case class Editable( with HasChildren with OnChangeEventHandler.CanHandleOnChangeEvent: type This = Editable - override def defaultEventHandler = - newValue => copy(valueReceived = Some(newValue)) override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -249,7 +247,6 @@ case class Input( ) extends ChakraElement with OnChangeEventHandler.CanHandleOnChangeEvent: type This = Input - override def defaultEventHandler = newValue => copy(valueReceived = Some(newValue)) override def withStyle(v: Map[String, Any]): Input = copy(style = v) def withKey(v: String): Input = copy(key = v) def withType(v: String): Input = copy(`type` = v) @@ -313,7 +310,6 @@ case class Checkbox( with OnChangeBooleanEventHandler.CanHandleOnChangeEvent: type This = Checkbox def checked: Boolean = checkedV.getOrElse(defaultChecked) - override def defaultEventHandler = newValue => copy(checkedV = Some(newValue.toBoolean)) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) @@ -348,7 +344,6 @@ case class RadioGroup( with HasChildren with OnChangeEventHandler.CanHandleOnChangeEvent: type This = RadioGroup - override def defaultEventHandler = newValue => copy(valueReceived = Some(newValue)) override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def value: String = valueReceived.getOrElse(defaultValue) @@ -1480,7 +1475,6 @@ case class Textarea( ) extends ChakraElement with OnChangeEventHandler.CanHandleOnChangeEvent: type This = Textarea - override def defaultEventHandler = newValue => copy(valueReceived = Some(newValue)) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withType(v: String) = copy(`type` = v) @@ -1505,7 +1499,6 @@ case class Switch( with OnChangeBooleanEventHandler.CanHandleOnChangeEvent: type This = Switch def checked: Boolean = checkedV.getOrElse(defaultChecked) - override def defaultEventHandler = newValue => copy(checkedV = Some(newValue.toBoolean)) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) @@ -1530,7 +1523,6 @@ case class Select( with HasChildren with OnChangeEventHandler.CanHandleOnChangeEvent: type This = Select - override def defaultEventHandler = newValue => copy(valueReceived = Some(newValue)) override def withStyle(v: Map[String, Any]) = copy(style = v) override def withChildren(cn: UiElement*) = copy(children = cn) def withKey(v: String) = copy(key = v) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala index 01b6bf54..d4e2e66f 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala @@ -84,7 +84,6 @@ case class Input( ) extends StdElement with CanHandleOnChangeEvent: type This = Input - override def defaultEventHandler = newValue => copy(valueReceived = Some(newValue)) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withType(v: String) = copy(`type` = v) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala index 0d91f73b..4ee3cb5a 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala @@ -2,7 +2,6 @@ package org.terminal21.client.components.std import org.terminal21.client.components.OnChangeEventHandler.CanHandleOnChangeEvent import org.terminal21.client.ConnectedSession -import org.terminal21.client.components.UiElement.HasEventHandler import org.terminal21.client.components.{EventHandler, Keys, OnChangeEventHandler, TransientRequest, UiElement} import org.terminal21.collections.TypedMap import org.terminal21.model.OnChange @@ -45,5 +44,4 @@ case class CookieReader( ) extends StdHttp with CanHandleOnChangeEvent: type This = CookieReader - override def defaultEventHandler = newValue => copy(value = Some(newValue)) override def withDataStore(ds: TypedMap): CookieReader = copy(dataStore = ds) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 36bd93ab..78a974ff 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -3,7 +3,7 @@ package org.terminal21.client import org.scalatest.funsuite.AnyFunSuiteLike import org.scalatest.matchers.should.Matchers.* import org.terminal21.client.components.UiElement -import org.terminal21.client.components.chakra.{Box, Button, Checkbox} +import org.terminal21.client.components.chakra.{Box, Button, ChakraElement, Checkbox} import org.terminal21.client.components.std.Input import org.terminal21.collections.SEList import org.terminal21.model.{ClientEvent, CommandEvent, OnChange, OnClick} @@ -20,18 +20,18 @@ class ControllerTest extends AnyFunSuiteLike: def newController[M]( initialModel: Model[M], events: => Seq[CommandEvent], - components: Seq[UiElement], + modelComponents: M => Seq[UiElement], renderChanges: Seq[UiElement] => Unit = _ => () ): Controller[M] = val seList = SEList[CommandEvent]() val it = seList.iterator events.foreach(e => seList.add(e)) seList.add(CommandEvent.sessionClosed) - new Controller(it, renderChanges, components, initialModel) + new Controller(it, renderChanges, modelComponents, initialModel, Nil) test("onEvent is called"): val model = Model(0) - newController(model, Seq(buttonClick), Seq(button)) + newController(model, Seq(buttonClick), _ => Seq(button)) .onEvent: event => if event.model > 1 then event.handled.terminate else event.handled.withModel(event.model + 1) .render() @@ -41,7 +41,7 @@ class ControllerTest extends AnyFunSuiteLike: test("onEvent is called for change"): val model = Model(0) - newController(model, Seq(inputChange), Seq(input)) + newController(model, Seq(inputChange), _ => Seq(input)) .onEvent: event => import event.* if event.model > 1 then handled.terminate else handled.withModel(event.model + 1) @@ -52,7 +52,7 @@ class ControllerTest extends AnyFunSuiteLike: test("onEvent not matched for change"): val model = Model(0) - newController(model, Seq(inputChange), Seq(input)) + newController(model, Seq(inputChange), _ => Seq(input)) .onEvent: case event: ControllerClickEvent[_] => import event.* @@ -64,7 +64,7 @@ class ControllerTest extends AnyFunSuiteLike: test("onEvent is called for change/boolean"): val model = Model(0) - newController(model, Seq(checkBoxChange), Seq(checkbox)) + newController(model, Seq(checkBoxChange), _ => Seq(checkbox)) .onEvent: event => import event.* if event.model > 1 then handled.terminate else handled.withModel(event.model + 1) @@ -75,7 +75,7 @@ class ControllerTest extends AnyFunSuiteLike: test("onEvent not matches for change/boolean"): val model = Model(0) - newController(model, Seq(checkBoxChange), Seq(checkbox)) + newController(model, Seq(checkBoxChange), _ => Seq(checkbox)) .onEvent: case event: ControllerClickEvent[_] => import event.* @@ -89,7 +89,7 @@ class ControllerTest extends AnyFunSuiteLike: test("onEvent is called for ClientEvent"): val model = Model(0) - newController(model, Seq(TestClientEvent(5)), Seq(button)) + newController(model, Seq(TestClientEvent(5)), _ => Seq(button)) .onEvent: case ControllerClientEvent(handled, event: TestClientEvent) => import event.* @@ -101,7 +101,7 @@ class ControllerTest extends AnyFunSuiteLike: test("onEvent when no partial function matches ClientEvent"): val model = Model(0) - newController(model, Seq(TestClientEvent(5)), Seq(button)) + newController(model, Seq(TestClientEvent(5)), _ => Seq(button)) .onEvent: case ControllerClickEvent(`checkbox`, handled) => handled.withModel(5).terminate @@ -115,10 +115,11 @@ class ControllerTest extends AnyFunSuiteLike: newController( model, Seq(buttonClick), - Seq( - button.onClick: event => - event.handled.withModel(100).terminate - ) + _ => + Seq( + button.onClick: event => + event.handled.withModel(100).terminate + ) ).render() .handledEventsIterator .map(_.model) @@ -129,10 +130,11 @@ class ControllerTest extends AnyFunSuiteLike: newController( model, Seq(inputChange), - Seq( - input.onChange: event => - event.handled.withModel(100).terminate - ) + _ => + Seq( + input.onChange: event => + event.handled.withModel(100).terminate + ) ).render() .handledEventsIterator .map(_.model) @@ -143,10 +145,11 @@ class ControllerTest extends AnyFunSuiteLike: newController( model, Seq(checkBoxChange), - Seq( - checkbox.onChange: event => - event.handled.withModel(100).terminate - ) + _ => + Seq( + checkbox.onChange: event => + event.handled.withModel(100).terminate + ) ).render() .handledEventsIterator .map(_.model) @@ -154,7 +157,7 @@ class ControllerTest extends AnyFunSuiteLike: test("terminate is obeyed and latest model state is iterated"): val model = Model(0) - newController(model, Seq(buttonClick, buttonClick, buttonClick), Seq(button)) + newController(model, Seq(buttonClick, buttonClick, buttonClick), _ => Seq(button)) .onEvent: event => if event.model > 1 then event.handled.terminate.withModel(100) else event.handled.withModel(event.model + 1) .render() @@ -165,144 +168,18 @@ class ControllerTest extends AnyFunSuiteLike: test("changes are rendered"): var rendered = Seq.empty[UiElement] def renderer(s: Seq[UiElement]): Unit = rendered = s + def components(m: Int) = Seq( + m match + case 0 => button + case 1 => button.withText("changed") + ) - newController(Model(0), Seq(buttonClick), Seq(button), renderer) + newController(Model(0), Seq(buttonClick), components, renderer) .onEvent: event => - event.handled.withModel(event.model + 1).withRenderChanges(button.withText("changed")).terminate + event.handled.withModel(event.model + 1).terminate .render() .handledEventsIterator .map(_.model) .toList should be(List(0, 1)) rendered should be(Seq(button.withText("changed"))) - - test("changes are rendered once"): - var rendered = Seq.empty[UiElement] - def renderer(s: Seq[UiElement]): Unit = rendered = s - - val model = Model(0) - val handled = newController( - model, - Seq(buttonClick, checkBoxChange), - Seq( - button.onClick(using model): event => - event.handled.withRenderChanges(button.withText("changed")), - checkbox - ), - renderer - ).render().handledEventsIterator.toList - - handled(1).renderChanges should be(List(button.withText("changed"))) - handled(2).renderChanges should be(Nil) - - test("timed changes are rendered"): - @volatile var rendered = Seq.empty[UiElement] - def renderer(s: Seq[UiElement]): Unit = rendered = s - newController(Model(0), Seq(buttonClick), Seq(button), renderer) - .onEvent: event => - event.handled.withModel(1).withTimedRenderChanges(TimedRenderChanges(10, button.withText("changed"))).terminate - .render() - .handledEventsIterator - .map(_.model) - .toList should be(List(0, 1)) - Thread.sleep(15) - rendered should be(Seq(button.withText("changed"))) - - test("timed changes are visible"): - val model = Model(0) - newController( - model, - Seq(buttonClick), - Seq( - button.onClick(using model): event => - event.handled.withTimedRenderChanges(TimedRenderChanges(10, button.withText("changed"))).terminate - ) - ).render().handledEventsIterator.toList(1).current(button) should be(button.withText("changed")) - - test("timed changes event handlers are called"): - val model = Model(0) - val c = checkbox.onChange(using model): event => - event.handled.withModel(2) - newController( - model, - Seq(buttonClick, checkBoxChange), - Seq( - button.onClick(using model): event => - event.handled.withTimedRenderChanges(TimedRenderChanges(10, c)) - ) - ).render() - .handledEventsIterator - .map(_.model) - .toList should be(List(0, 0, 2)) - - test("current value for OnChange"): - val model = Model(0) - newController( - model, - Seq(inputChange), - Seq( - input.onChange(using model): event => - import event.* - handled.withModel(if input.current.value == "new-value" then 100 else -1).terminate - ) - ).render() - .handledEventsIterator - .map(_.model) - .toList should be(List(0, 100)) - - test("current value for OnChange/boolean"): - val model = Model(0) - newController( - model, - Seq(checkBoxChange), - Seq( - checkbox.onChange(using model): event => - import event.* - handled.withModel(if checkbox.current.checked then 100 else -1).terminate - ) - ).render() - .handledEventsIterator - .map(_.model) - .toList should be(List(0, 100)) - - test("newly rendered elements are visible"): - val model = Model(0) - lazy val box: Box = Box().withChildren( - button.onClick(using model): event => - event.handled.withRenderChanges(box.withChildren(button, checkbox)) - ) - - val handledEvents = newController(model, Seq(buttonClick), Seq(box)).render().handledEventsIterator.toList - handledEvents(1).componentsByKey(checkbox.key) should be(checkbox) - - test("newly rendered elements event handlers are invoked"): - val model = Model(0) - lazy val b: Button = button.onClick(using model): event => - event.handled - .withModel(1) - .withRenderChanges( - box.withChildren( - b, - checkbox.onChange(using model): event => - event.handled.withModel(2) - ) - ) - - lazy val box: Box = Box().withChildren(b) - - newController(model, Seq(buttonClick, checkBoxChange), Seq(box)) - .render() - .handledEventsIterator - .map(_.model) - .toList should be(List(0, 1, 2)) - - test("RenderChangesEvent renders changes"): - var rendered = Seq.empty[UiElement] - def renderer(s: Seq[UiElement]): Unit = rendered = s - - newController(Model(0), Seq(RenderChangesEvent(Seq(button.withText("changed")))), Seq(button), renderer) - .render() - .handledEventsIterator - .map(_.model) - .toList should be(List(0, 0)) - rendered should be(Seq(button.withText("changed"))) From 4e1842ce9b9828caeef03a3d094715924a27b22b Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 27 Feb 2024 22:32:09 +0000 Subject: [PATCH 198/313] - --- .../terminal21/serverapp/bundled/ServerStatusApp.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 585653c2..f1720d0b 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -28,13 +28,13 @@ class ServerStatusPage( val initModel = StatusModel(Runtime.getRuntime, sessionsService.allSessions) given Model[StatusModel] = Model(initModel) - case object Ticker extends ClientEvent + case class Ticker(sessions: Seq[Session]) extends ClientEvent def run(): Unit = fiberExecutor.submit: while !appSession.isClosed do Thread.sleep(2000) - appSession.fireEvents(Ticker) + appSession.fireEvents(Ticker(sessionsService.allSessions)) try controller.render().handledEventsIterator.lastOption catch case t: Throwable => t.printStackTrace() @@ -44,8 +44,8 @@ class ServerStatusPage( def controller: Controller[StatusModel] = Controller(components).onEvent: - case ControllerClientEvent(handled, Ticker) => - handled.withModel(handled.model.copy(sessions = sessionsService.allSessions)) + case ControllerClientEvent(handled, Ticker(sessions)) => + handled.withModel(handled.model.copy(sessions = sessions)) def components(m: StatusModel): Seq[UiElement] = Seq(jvmTable(m.runtime), sessionsTable(m.sessions)) @@ -80,7 +80,7 @@ class ServerStatusPage( private def actionsFor(session: Session): UiElement = if session.isOpen then - Box().withChildren( + Box(key = s"session-${session.id}-actions").withChildren( Button(key = s"close-${session.id}", text = "Close", size = xs) .withLeftIcon(SmallCloseIcon()) .onClick: event => From 9f52d3d78798cf31c4d0d256337398035f61282c Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 28 Feb 2024 01:37:09 +0000 Subject: [PATCH 199/313] - --- Readme.md | 2 +- .../serverapp/bundled/AppManager.scala | 1 + .../org/terminal21/ui/std/ServerJson.scala | 3 +- .../terminal21/client/ConnectedSession.scala | 6 ++- .../org/terminal21/client/Controller.scala | 40 ++++++++++++------- .../client/components/UiElement.scala | 1 + .../components/chakra/QuickFormControl.scala | 1 + .../client/components/chakra/QuickTabs.scala | 1 + .../components/frontend/FrontEndElement.scala | 4 +- .../client/components/std/StdHttp.scala | 6 ++- 10 files changed, 45 insertions(+), 20 deletions(-) diff --git a/Readme.md b/Readme.md index b72c471f..d6f57855 100644 --- a/Readme.md +++ b/Readme.md @@ -172,7 +172,7 @@ Please use the [discussions](https://github.com/kostaskougios/terminal21-restapi - session builders refactoring for more flexible creation of sessions - QuickTabs, QuickFormControl - bug fix for old react state re-rendering on new session -- event iterators allows idiomatic handling of events and overhaul of the event handling for easier testing +- event iterators allows idiomatic handling of events and overhaul of the event handling for easier testing and easier development of larger apps ## Version 0.21 diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala index cd02ae0f..1d954183 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala @@ -41,6 +41,7 @@ class AppManagerPage(apps: Seq[ServerSideApp], startApp: ServerSideApp => Unit)( def components(m: ManagerModel): Seq[UiElement] = val appsTable = QuickTable( + key = "apps-table", caption = Some("Apps installed on the server, click one to run it."), rows = appRows ).withHeaders("App Name", "Description") diff --git a/terminal21-ui-std-exports/src/main/scala/org/terminal21/ui/std/ServerJson.scala b/terminal21-ui-std-exports/src/main/scala/org/terminal21/ui/std/ServerJson.scala index abdae823..661e56fb 100644 --- a/terminal21-ui-std-exports/src/main/scala/org/terminal21/ui/std/ServerJson.scala +++ b/terminal21-ui-std-exports/src/main/scala/org/terminal21/ui/std/ServerJson.scala @@ -32,7 +32,8 @@ case class ServerJson( |${toHumanReadableString} |The received: |${j.toHumanReadableString} - |""".stripMargin + |""".stripMargin, + t ) throw t diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index 871cba33..bf56f026 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -1,6 +1,7 @@ package org.terminal21.client import org.terminal21.client.components.UiElement.HasChildren +import org.terminal21.client.components.chakra.Box import org.terminal21.client.components.{UiComponent, UiElement} import org.terminal21.client.json.UiElementEncoding import org.terminal21.collections.SEList @@ -107,9 +108,10 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se sessionsService.changeSessionJsonState(session, j) private def toJson(elements: Seq[UiElement]): ServerJson = - val flat = elements.flatMap(_.flat) + val root = Box(key = "root", children = elements) // keep the root element with a steady key + val flat = root.flat val sj = ServerJson( - elements.map(_.key), + Seq(root.key), flat .map: el => ( diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 428fb5ff..e14dedd5 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -16,8 +16,9 @@ class Controller[M]( eventHandlers: Seq[PartialFunction[ControllerEvent[M], HandledEvent[M]]] ): def render()(using session: ConnectedSession): RenderedController[M] = - session.render(modelComponents(initialModel.value)) - new RenderedController(eventIteratorFactory, initialModel, modelComponents, renderChanges, eventHandlers) + val initComponents = modelComponents(initialModel.value) + session.render(initComponents) + new RenderedController(eventIteratorFactory, initialModel, initComponents, modelComponents, renderChanges, eventHandlers) def onEvent(handler: PartialFunction[ControllerEvent[M], HandledEvent[M]]) = new Controller( @@ -31,6 +32,7 @@ class Controller[M]( class RenderedController[M]( eventIteratorFactory: => Iterator[CommandEvent], initialModel: Model[M], + initialComponents: Seq[UiElement], modelComponents: M => Seq[UiElement], renderChanges: Seq[UiElement] => Unit, eventHandlers: Seq[PartialFunction[ControllerEvent[M], HandledEvent[M]]] @@ -70,7 +72,7 @@ class RenderedController[M]( if f.isDefinedAt(e) then f(e) else h case x => throw new IllegalStateException(s"Unexpected state $x") - private def invokeComponentEventHandlers(h: HandledEvent[M], event: CommandEvent) = + private def invokeComponentEventHandlers(h: HandledEvent[M], event: CommandEvent): HandledEvent[M] = lazy val clickHandlers = clickHandlersMap(h) lazy val changeHandlers = changeHandlersMap(h) lazy val changeBooleanHandlers = changeBooleanHandlersMap(h) @@ -95,8 +97,8 @@ class RenderedController[M]( handled case _ => h - private def initialComponentsByKeyMap: Map[String, UiElement] = - val all = modelComponents(initialModel.value) + private def calcComponentsByKeyMap(components: Seq[UiElement]): Map[String, UiElement] = + val all = components .flatMap(_.flat) .map(c => (c.key, c)) .toMap @@ -106,19 +108,29 @@ class RenderedController[M]( ) ) + private def doRenderChanges(oldHandled: HandledEvent[M], newHandled: HandledEvent[M]): HandledEvent[M] = + // TODO: optimise what elements are rendered + val all = modelComponents(newHandled.model) + renderChanges(all) + newHandled.copy(componentsByKey = calcComponentsByKeyMap(all)) + def handledEventsIterator: EventIterator[HandledEvent[M]] = + val initHandled = HandledEvent(initialModel.value, calcComponentsByKeyMap(initialComponents), false) new EventIterator( eventIteratorFactory .takeWhile(!_.isSessionClosed) - .scanLeft(HandledEvent(initialModel.value, initialComponentsByKeyMap, false)): (oldHandled, event) => - try - val handled2 = invokeEventHandlers(oldHandled, event) - val handled3 = invokeComponentEventHandlers(handled2, event) - handled3 - catch - case t: Throwable => - logger.error("an error occurred while iterating events", t) - oldHandled + .scanLeft((initHandled, initHandled)): + case ((_, oldHandled), event) => + try + val handled2 = invokeEventHandlers(oldHandled, event) + val handled3 = invokeComponentEventHandlers(handled2, event) + (oldHandled, handled3) + catch + case t: Throwable => + logger.error("an error occurred while iterating events", t) + (oldHandled, oldHandled) + .map: (oldHandled, newHandled) => + if oldHandled.model != newHandled.model then doRenderChanges(oldHandled, newHandled) else newHandled .flatMap: h => // trick to make sure we take the last state of the model when shouldTerminate=true if h.shouldTerminate then Seq(h.copy(shouldTerminate = false), h) else Seq(h) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala index 65b5dfb0..dea57e98 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala @@ -6,6 +6,7 @@ trait UiElement extends AnyElement: type This <: UiElement def key: String + def withKey(key: String): This /** @return * this element along all it's children flattened diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala index bf0eb09e..39abf492 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala @@ -26,3 +26,4 @@ case class QuickFormControl( def withHelperText(text: String): QuickFormControl = copy(helperText = Some(text)) override def withStyle(v: Map[String, Any]): QuickFormControl = copy(style = v) + override def withKey(key: String): QuickFormControl = copy(key = key) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala index fa835881..f31ffa12 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala @@ -32,3 +32,4 @@ case class QuickTabs( ) override def withStyle(v: Map[String, Any]): QuickTabs = copy(style = v) + override def withKey(key: String): QuickTabs = copy(key = key) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/frontend/FrontEndElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/frontend/FrontEndElement.scala index d043758e..b48b82f2 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/frontend/FrontEndElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/frontend/FrontEndElement.scala @@ -4,4 +4,6 @@ import org.terminal21.client.components.{Keys, UiElement} sealed trait FrontEndElement extends UiElement -case class ThemeToggle(key: String = Keys.nextKey) extends FrontEndElement +case class ThemeToggle(key: String = Keys.nextKey) extends FrontEndElement: + override type This = ThemeToggle + override def withKey(key: String): ThemeToggle = copy(key = key) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala index 4ee3cb5a..e952e405 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala @@ -30,7 +30,9 @@ case class Cookie( path: Option[String] = None, expireDays: Option[Int] = None, requestId: String = TransientRequest.newRequestId() -) extends StdHttp +) extends StdHttp: + override type This = Cookie + override def withKey(key: String): Cookie = copy(key = key) /** Read a cookie value. The value, when read from the ui, it will reflect in `value` assuming the UI had the time to send the value back. Also the onChange * handler will be called once with the value. @@ -45,3 +47,5 @@ case class CookieReader( with CanHandleOnChangeEvent: type This = CookieReader override def withDataStore(ds: TypedMap): CookieReader = copy(dataStore = ds) + + override def withKey(key: String): CookieReader = copy(key = key) From f9f3726957b9627c1e9cf8aa4d36479cf376a666 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 28 Feb 2024 11:32:41 +0000 Subject: [PATCH 200/313] - --- .../org/terminal21/client/Controller.scala | 14 +++++++--- .../terminal21/client/components/Keys.scala | 17 +++++++++-- .../components/chakra/QuickFormControl.scala | 5 ++-- .../client/components/chakra/QuickTable.scala | 3 +- .../client/components/chakra/QuickTabs.scala | 28 +++++++++++-------- .../terminal21/client/ControllerTest.scala | 12 +++++--- .../client/components/KeysTest.scala | 22 +++++++++++++++ 7 files changed, 74 insertions(+), 27 deletions(-) create mode 100644 terminal21-ui-std/src/test/scala/org/terminal21/client/components/KeysTest.scala diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index e14dedd5..8aa06e21 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -4,7 +4,7 @@ import org.slf4j.LoggerFactory import org.terminal21.client.collections.EventIterator import org.terminal21.client.components.OnChangeEventHandler.CanHandleOnChangeEvent import org.terminal21.client.components.OnClickEventHandler.CanHandleOnClickEvent -import org.terminal21.client.components.{OnChangeBooleanEventHandler, OnChangeEventHandler, OnClickEventHandler, UiElement} +import org.terminal21.client.components.{Keys, OnChangeBooleanEventHandler, OnChangeEventHandler, OnClickEventHandler, UiElement} import org.terminal21.collections.TypedMapKey import org.terminal21.model.{ClientEvent, CommandEvent, OnChange, OnClick} @@ -16,7 +16,7 @@ class Controller[M]( eventHandlers: Seq[PartialFunction[ControllerEvent[M], HandledEvent[M]]] ): def render()(using session: ConnectedSession): RenderedController[M] = - val initComponents = modelComponents(initialModel.value) + val initComponents = Keys.linearKeys(modelComponents(initialModel.value)) session.render(initComponents) new RenderedController(eventIteratorFactory, initialModel, initComponents, modelComponents, renderChanges, eventHandlers) @@ -97,9 +97,15 @@ class RenderedController[M]( handled case _ => h + private def checkForDuplicatesAndThrow(seq: Seq[String]): Unit = + val duplicates = seq.groupBy(identity).filter(_._2.size > 1).keys.toList + if duplicates.nonEmpty then throw new IllegalArgumentException(s"Duplicate(s) found: ${duplicates.mkString(", ")}") + private def calcComponentsByKeyMap(components: Seq[UiElement]): Map[String, UiElement] = - val all = components + val flattened = components .flatMap(_.flat) + checkForDuplicatesAndThrow(flattened.map(_.key)) + val all = flattened .map(c => (c.key, c)) .toMap all.withDefault(key => @@ -110,7 +116,7 @@ class RenderedController[M]( private def doRenderChanges(oldHandled: HandledEvent[M], newHandled: HandledEvent[M]): HandledEvent[M] = // TODO: optimise what elements are rendered - val all = modelComponents(newHandled.model) + val all = Keys.linearKeys(modelComponents(newHandled.model)) renderChanges(all) newHandled.copy(componentsByKey = calcComponentsByKeyMap(all)) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/Keys.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/Keys.scala index 4ef4ce23..9dd161b3 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/Keys.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/Keys.scala @@ -1,7 +1,18 @@ package org.terminal21.client.components -import java.util.concurrent.atomic.AtomicInteger +import org.terminal21.client.components.UiElement.HasChildren object Keys: - private val keyId = new AtomicInteger(0) - def nextKey: String = s"key${keyId.incrementAndGet()}" + def nextKey: String = "" + + def linearKeys(parentKey: String, element: UiElement): Seq[UiElement] = linearKeys(parentKey, Seq(element)) + def linearKeys(elements: Seq[UiElement]): Seq[UiElement] = linearKeys(None, elements) + def linearKeys(parentKey: String, elements: Seq[UiElement]): Seq[UiElement] = linearKeys(Some(parentKey), elements) + def linearKeys(parentKey: Option[String], elements: Seq[UiElement]): Seq[UiElement] = + val pk = parentKey.map(_ + "-").getOrElse("k-") + elements.zipWithIndex.map: + case (e, i) => + val p = if e.key.isEmpty then e.withKey(s"$pk$i") else e + p match + case wc: HasChildren => wc.withChildren(linearKeys(Some(p.key), wc.children)*) + case n => n diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala index 39abf492..602567a3 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala @@ -1,5 +1,6 @@ package org.terminal21.client.components.chakra +import org.terminal21.client.components.Keys.linearKeys import org.terminal21.client.components.{Keys, UiComponent, UiElement} import org.terminal21.client.components.UiElement.HasStyle @@ -17,9 +18,7 @@ case class QuickFormControl( label.map(l => FormLabel(key = key + "-label", text = l)).toSeq ++ Seq(InputGroup(key = key + "-ig").withChildren(inputGroup: _*)) ++ helperText.map(h => FormHelperText(key = key + "-helper", text = h)) - Seq( - FormControl(key = key + "-fc", style = style).withChildren(ch: _*) - ) + linearKeys(key, FormControl(key = key + "-fc", style = style).withChildren(ch: _*)) def withLabel(label: String): QuickFormControl = copy(label = Some(label)) def withInputGroup(ig: UiElement*): QuickFormControl = copy(inputGroup = ig) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala index e4d11748..5a37b43b 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala @@ -1,5 +1,6 @@ package org.terminal21.client.components.chakra +import org.terminal21.client.components.Keys.linearKeys import org.terminal21.client.components.UiElement.HasStyle import org.terminal21.client.components.{Keys, UiComponent, UiElement} @@ -37,7 +38,7 @@ case class QuickTable( children = caption.map(text => TableCaption(text = text)).toSeq ++ Seq(head, body) ) val tableContainer = TableContainer(key = key + "-tc", style = style, children = Seq(table)) - Seq(tableContainer) + linearKeys(key, tableContainer) def withHeaders(headers: String*): QuickTable = copy(headers = headers.map(h => Text(text = h))) def withHeadersElements(headers: UiElement*): QuickTable = copy(headers = headers) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala index f31ffa12..c74f8510 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala @@ -1,5 +1,6 @@ package org.terminal21.client.components.chakra +import org.terminal21.client.components.Keys.linearKeys import org.terminal21.client.components.UiElement.HasStyle import org.terminal21.client.components.{Keys, UiComponent, UiElement} @@ -15,18 +16,21 @@ case class QuickTabs( def withTabs(tabs: String | Seq[UiElement]*): QuickTabs = copy(tabs = tabs) def withTabPanels(tabPanels: Seq[UiElement]*): QuickTabs = copy(tabPanels = tabPanels) - override lazy val rendered = Seq( - Tabs(key = key + "-tabs", style = style).withChildren( - TabList( - key = key + "-tab-list", - children = tabs.zipWithIndex.map: - case (name: String, idx) => Tab(key = s"$key-tab-$idx", text = name) - case (elements: Seq[UiElement], idx) => Tab(key = s"$key-tab-$idx", children = elements) - ), - TabPanels( - key = key + "-panels", - children = tabPanels.zipWithIndex.map: (elements, idx) => - TabPanel(key = s"$key-panel-$idx", children = elements) + override lazy val rendered = linearKeys( + key, + Seq( + Tabs(key = key + "-tabs", style = style).withChildren( + TabList( + key = key + "-tab-list", + children = tabs.zipWithIndex.map: + case (name: String, idx) => Tab(key = s"$key-tab-$idx", text = name) + case (elements: Seq[UiElement], idx) => Tab(key = s"$key-tab-$idx", children = elements) + ), + TabPanels( + key = key + "-panels", + children = tabPanels.zipWithIndex.map: (elements, idx) => + TabPanel(key = s"$key-panel-$idx", children = elements) + ) ) ) ) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 78a974ff..9c28eeba 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -3,17 +3,17 @@ package org.terminal21.client import org.scalatest.funsuite.AnyFunSuiteLike import org.scalatest.matchers.should.Matchers.* import org.terminal21.client.components.UiElement -import org.terminal21.client.components.chakra.{Box, Button, ChakraElement, Checkbox} +import org.terminal21.client.components.chakra.{Button, Checkbox} import org.terminal21.client.components.std.Input import org.terminal21.collections.SEList import org.terminal21.model.{ClientEvent, CommandEvent, OnChange, OnClick} class ControllerTest extends AnyFunSuiteLike: - val button = Button() + val button = Button(key = "b1") val buttonClick = OnClick(button.key) - val input = Input() + val input = Input(key = "i1") val inputChange = OnChange(input.key, "new-value") - val checkbox = Checkbox() + val checkbox = Checkbox(key = "c1") val checkBoxChange = OnChange(checkbox.key, "true") given ConnectedSession = ConnectedSessionMock.newConnectedSessionMock @@ -29,6 +29,10 @@ class ControllerTest extends AnyFunSuiteLike: seList.add(CommandEvent.sessionClosed) new Controller(it, renderChanges, modelComponents, initialModel, Nil) + test("will throw an exception if there is a duplicate key"): + an[IllegalArgumentException] should be thrownBy + newController(Model(0), Seq(buttonClick), _ => Seq(button, button)).render().handledEventsIterator + test("onEvent is called"): val model = Model(0) newController(model, Seq(buttonClick), _ => Seq(button)) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/components/KeysTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/components/KeysTest.scala new file mode 100644 index 00000000..6fdbb948 --- /dev/null +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/components/KeysTest.scala @@ -0,0 +1,22 @@ +package org.terminal21.client.components + +import org.scalatest.funsuite.AnyFunSuiteLike +import org.terminal21.client.components.chakra.{Box, Text} +import org.scalatest.matchers.should.Matchers.* + +class KeysTest extends AnyFunSuiteLike: + test("doesn't reassign key to a defined key"): + val b = Box(key = "k1") + Keys.linearKeys(Seq(b)) should be(Seq(b)) + + test("assign key"): + val b = Box() + Keys.linearKeys(Seq(b)) should be(Seq(b.withKey("k-0"))) + + test("assign key to children"): + val b = Box().withChildren(Text(), Text()) + Keys.linearKeys(Seq(b)) should be( + Seq( + b.withKey("k-0").withChildren(Text().withKey("k-0-0"), Text().withKey("k-0-1")) + ) + ) From 8f38654292f1f3657803b42309ae6c175146f26d Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 28 Feb 2024 11:51:37 +0000 Subject: [PATCH 201/313] - --- .../serverapp/bundled/AppManager.scala | 3 +- .../bundled/AppManagerPageTest.scala | 124 +++++++++--------- 2 files changed, 65 insertions(+), 62 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala index 1d954183..4e008084 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala @@ -32,7 +32,7 @@ class AppManagerPage(apps: Seq[ServerSideApp], startApp: ServerSideApp => Unit)( val appRows: Seq[Seq[UiElement]] = apps.map: app => Seq( - Link(text = app.name).onClick: event => + Link(key = s"app-${app.name}", text = app.name).onClick: event => import event.* handled.withModel(model.copy(startApp = Some(app))) , @@ -56,6 +56,7 @@ class AppManagerPage(apps: Seq[ServerSideApp], startApp: ServerSideApp => Unit)( Paragraph().withChildren( Span(text = "Have a question? Please ask at "), Link( + key = "discussion-board-link", text = "terminal21's discussion board ", href = "https://github.com/kostaskougios/terminal21-restapi/discussions", color = Some("teal.500"), diff --git a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala index c72dfbf1..12f7c66e 100644 --- a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala +++ b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala @@ -1,61 +1,63 @@ -//package org.terminal21.serverapp.bundled -// -//import org.mockito.Mockito -//import org.mockito.Mockito.when -//import org.scalatest.funsuite.AnyFunSuiteLike -//import org.scalatestplus.mockito.MockitoSugar.mock -//import org.terminal21.client.components.* -//import org.terminal21.client.components.chakra.{Link, Text} -//import org.terminal21.client.{ConnectedSession, ConnectedSessionMock} -//import org.terminal21.serverapp.ServerSideApp -//import org.scalatest.matchers.should.Matchers.* -//import org.terminal21.model.CommandEvent -// -//class AppManagerPageTest extends AnyFunSuiteLike: -// def mockApp(name: String, description: String) = -// val app = mock[ServerSideApp] -// when(app.name).thenReturn(name) -// when(app.description).thenReturn(description) -// app -// -// class App(apps: ServerSideApp*): -// given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock -// var startedApp: Option[ServerSideApp] = None -// val page = new AppManagerPage(apps, app => startedApp = Some(app)) -// -// test("renders app links"): -// new App(mockApp("app1", "the-app1-desc")): -// allComponents -// .collect: -// case l: Link if l.text == "app1" => l -// .size should be(1) -// -// test("renders app description"): -// new App(mockApp("app1", "the-app1-desc")): -// allComponents -// .collect: -// case t: Text if t.text == "the-app1-desc" => t -// .size should be(1) -// -// test("renders the discussions link"): -// new App(): -// allComponents -// .collect: -// case l: Link if l.href == "https://github.com/kostaskougios/terminal21-restapi/discussions" => l -// .size should be(1) -// -// test("starts app when app link is clicked"): -// val app = mockApp("app1", "the-app1-desc") -// new App(app): -// val eventsIt = page.eventsIterator -// session.fireEvents(CommandEvent.onClick(page.appRows.head.head), CommandEvent.sessionClosed) -// eventsIt.toList -// startedApp should be(Some(app)) -// -// test("resets startApp state on other events"): -// val app = mockApp("app1", "the-app1-desc") -// new App(app): -// val other = Link() -// val eventsIt = page.controller(page.components :+ other).render().handledEventsIterator -// session.fireEvents(CommandEvent.onClick(page.appRows.head.head), CommandEvent.onClick(other), CommandEvent.sessionClosed) -// eventsIt.toList.map(_.model).last.startApp should be(None) +package org.terminal21.serverapp.bundled + +import org.mockito.Mockito +import org.mockito.Mockito.when +import org.scalatest.funsuite.AnyFunSuiteLike +import org.scalatestplus.mockito.MockitoSugar.mock +import org.terminal21.client.components.* +import org.terminal21.client.components.chakra.{Link, Text} +import org.terminal21.client.{ConnectedSession, ConnectedSessionMock} +import org.terminal21.serverapp.ServerSideApp +import org.scalatest.matchers.should.Matchers.* +import org.terminal21.model.CommandEvent + +class AppManagerPageTest extends AnyFunSuiteLike: + def mockApp(name: String, description: String) = + val app = mock[ServerSideApp] + when(app.name).thenReturn(name) + when(app.description).thenReturn(description) + app + + class App(apps: ServerSideApp*): + given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock + var startedApp: Option[ServerSideApp] = None + val page = new AppManagerPage(apps, app => startedApp = Some(app)) + val model = page.ManagerModel() + def allComponents = page.components(model).flatMap(_.flat) + + test("renders app links"): + new App(mockApp("app1", "the-app1-desc")): + allComponents + .collect: + case l: Link if l.text == "app1" => l + .size should be(1) + + test("renders app description"): + new App(mockApp("app1", "the-app1-desc")): + allComponents + .collect: + case t: Text if t.text == "the-app1-desc" => t + .size should be(1) + + test("renders the discussions link"): + new App(): + allComponents + .collect: + case l: Link if l.href == "https://github.com/kostaskougios/terminal21-restapi/discussions" => l + .size should be(1) + + test("starts app when app link is clicked"): + val app = mockApp("app1", "the-app1-desc") + new App(app): + val eventsIt = page.eventsIterator + session.fireEvents(CommandEvent.onClick(page.appRows.head.head), CommandEvent.sessionClosed) + eventsIt.toList + startedApp should be(Some(app)) + + test("resets startApp state on other events"): + val app = mockApp("app1", "the-app1-desc") + new App(app): + val other = allComponents.find(_.key == "discussion-board-link").get + val eventsIt = page.controller.render().handledEventsIterator + session.fireEvents(CommandEvent.onClick(page.appRows.head.head), CommandEvent.onClick(other), CommandEvent.sessionClosed) + eventsIt.toList.map(_.model).last.startApp should be(None) From a10c416df60abea737eccbb262be9f750ffc61c3 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 28 Feb 2024 12:06:34 +0000 Subject: [PATCH 202/313] - --- .../serverapp/bundled/ServerStatusApp.scala | 6 +- .../bundled/ServerStatusPageTest.scala | 151 +++++++++--------- 2 files changed, 75 insertions(+), 82 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index f1720d0b..97339279 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -52,7 +52,7 @@ class ServerStatusPage( private val jvmTableE = QuickTable(key = "jvmTable", caption = Some("JVM")) .withHeaders("Property", "Value", "Actions") - private val gcButton = Button(size = xs, text = "Run GC") + private val gcButton = Button(key = "gc-button", size = xs, text = "Run GC") .onClick: event => System.gc() event.handled @@ -80,7 +80,7 @@ class ServerStatusPage( private def actionsFor(session: Session): UiElement = if session.isOpen then - Box(key = s"session-${session.id}-actions").withChildren( + Box().withChildren( Button(key = s"close-${session.id}", text = "Close", size = xs) .withLeftIcon(SmallCloseIcon()) .onClick: event => @@ -89,7 +89,7 @@ class ServerStatusPage( handled , Text(text = " "), - Button(key = s"view-${session.id}", text = "View State", size = xs) + Button(text = "View State", size = xs) .withLeftIcon(ChatIcon()) .onClick: event => serverSideSessions diff --git a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala index d7ea3c5e..b90951c1 100644 --- a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala +++ b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala @@ -1,79 +1,72 @@ -//package org.terminal21.serverapp.bundled -// -//import org.scalatest.funsuite.AnyFunSuiteLike -//import org.scalatestplus.mockito.MockitoSugar.mock -//import org.terminal21.client.components.chakra.{Button, CheckIcon, NotAllowedIcon, Text} -//import org.terminal21.client.{ConnectedSession, ConnectedSessionMock} -//import org.terminal21.model.CommonModelBuilders.session -//import org.terminal21.model.{CommandEvent, CommonModelBuilders, Session} -//import org.terminal21.server.service.ServerSessionsService -//import org.terminal21.serverapp.ServerSideSessions -//import org.terminal21.client.given -//import org.scalatest.matchers.should.Matchers.* -// -//class ServerStatusPageTest extends AnyFunSuiteLike: -// class App: -// given connectedSession: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock -// val sessionsService = mock[ServerSessionsService] -// val serverSideSessions = mock[ServerSideSessions] -// val page = new ServerStatusPage(serverSideSessions, sessionsService) -// -// test("Close button for a session"): -// new App: -// page -// .sessionsTable(Seq(session())) -// .flat -// .collectFirst: -// case b: Button if b.text == "Close" => b -// .isEmpty should be(false) -// -// test("View state button for a session"): -// new App: -// page -// .sessionsTable(Seq(session())) -// .flat -// .collectFirst: -// case b: Button if b.text == "View State" => b -// .isEmpty should be(false) -// -// test("When session is open, a CheckIcon is displayed"): -// new App: -// page -// .sessionsTable(Seq(session())) -// .flat -// .collectFirst: -// case i: CheckIcon => i -// .isEmpty should be(false) -// -// test("When session is closed, a NotAllowedIcon is displayed"): -// new App: -// page -// .sessionsTable(Seq(session(isOpen = false))) -// .flat -// .collectFirst: -// case i: NotAllowedIcon => i -// .isEmpty should be(false) -// -// test("sessions are rendered when Ticker event is fired"): -// new App: -// var times = 0 -// def sessions = -// times += 1 -// times match -// case 1 => Seq(session(id = "s1", name = "session 1")) // this is initially rendered -// case 2 => Seq(session(id = "s2", name = "session 2")) // this is a change -// case 3 => Seq(session(id = "s3", name = "session 3")) // this is also a change -// val it = page.controller(Runtime.getRuntime, sessions).render().handledEventsIterator -// connectedSession.fireEvents(page.Ticker, page.Ticker, CommandEvent.sessionClosed) -// val handledEvents = it.toList -// handledEvents.head.renderChanges should be(Nil) -// handledEvents(1).renderChanges -// .flatMap(_.flat) -// .collectFirst: -// case t: Text if t.text == "session 2" => t -// .size should be(1) -// handledEvents(2).renderChanges -// .flatMap(_.flat) -// .collectFirst: -// case t: Text if t.text == "session 3" => t -// .size should be(1) +package org.terminal21.serverapp.bundled + +import org.mockito.Mockito.when +import org.scalatest.funsuite.AnyFunSuiteLike +import org.scalatestplus.mockito.MockitoSugar.mock +import org.terminal21.client.components.chakra.{Button, CheckIcon, NotAllowedIcon, Text} +import org.terminal21.client.{ConnectedSession, ConnectedSessionMock} +import org.terminal21.model.CommonModelBuilders.session +import org.terminal21.model.{CommandEvent, CommonModelBuilders, Session} +import org.terminal21.server.service.ServerSessionsService +import org.terminal21.serverapp.ServerSideSessions +import org.terminal21.client.given +import org.scalatest.matchers.should.Matchers.* + +class ServerStatusPageTest extends AnyFunSuiteLike: + class App: + given connectedSession: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock + val sessionsService = mock[ServerSessionsService] + val serverSideSessions = mock[ServerSideSessions] + when(sessionsService.allSessions).thenReturn(Seq(session(id = "session1"))) + val page = new ServerStatusPage(serverSideSessions, sessionsService) + + test("Close button for a session"): + new App: + page + .sessionsTable(Seq(session())) + .flat + .collectFirst: + case b: Button if b.text == "Close" => b + .isEmpty should be(false) + + test("View state button for a session"): + new App: + page + .sessionsTable(Seq(session())) + .flat + .collectFirst: + case b: Button if b.text == "View State" => b + .isEmpty should be(false) + + test("When session is open, a CheckIcon is displayed"): + new App: + page + .sessionsTable(Seq(session())) + .flat + .collectFirst: + case i: CheckIcon => i + .isEmpty should be(false) + + test("When session is closed, a NotAllowedIcon is displayed"): + new App: + page + .sessionsTable(Seq(session(isOpen = false))) + .flat + .collectFirst: + case i: NotAllowedIcon => i + .isEmpty should be(false) + + test("sessions are rendered when Ticker event is fired"): + new App: + val it = page.controller.render().handledEventsIterator + private val sessions2 = Seq(session(id = "s2", name = "session 2")) + private val sessions3 = Seq(session(id = "s3", name = "session 3")) + connectedSession.fireEvents( + page.Ticker(sessions2), + page.Ticker(sessions3), + CommandEvent.sessionClosed + ) + val handledEvents = it.toList + handledEvents.head.model.sessions should be(Seq(session(id = "session1"))) + handledEvents(1).model.sessions should be(sessions2) + handledEvents(2).model.sessions should be(sessions3) From 7fb4c183bffe42a0981ed688d43416f3dd786302 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 28 Feb 2024 12:19:06 +0000 Subject: [PATCH 203/313] - --- .../serverapp/bundled/ServerStatusPageTest.scala | 13 +++++++++++-- .../terminal21/client/components/UiElement.scala | 1 + .../client/components/UiElementTest.scala | 14 ++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 terminal21-ui-std/src/test/scala/org/terminal21/client/components/UiElementTest.scala diff --git a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala index b90951c1..3905d11c 100644 --- a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala +++ b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala @@ -1,6 +1,6 @@ package org.terminal21.serverapp.bundled -import org.mockito.Mockito.when +import org.mockito.Mockito.{verify, when} import org.scalatest.funsuite.AnyFunSuiteLike import org.scalatestplus.mockito.MockitoSugar.mock import org.terminal21.client.components.chakra.{Button, CheckIcon, NotAllowedIcon, Text} @@ -17,7 +17,8 @@ class ServerStatusPageTest extends AnyFunSuiteLike: given connectedSession: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock val sessionsService = mock[ServerSessionsService] val serverSideSessions = mock[ServerSideSessions] - when(sessionsService.allSessions).thenReturn(Seq(session(id = "session1"))) + val session1 = session(id = "session1") + when(sessionsService.allSessions).thenReturn(Seq(session1)) val page = new ServerStatusPage(serverSideSessions, sessionsService) test("Close button for a session"): @@ -70,3 +71,11 @@ class ServerStatusPageTest extends AnyFunSuiteLike: handledEvents.head.model.sessions should be(Seq(session(id = "session1"))) handledEvents(1).model.sessions should be(sessions2) handledEvents(2).model.sessions should be(sessions3) + + test("closes session when close button is clicked"): + new App: + val it = page.controller.render().handledEventsIterator + connectedSession.fireClickEvent(page.sessionsTable(Seq(session1)).findKey("close-session1")) + connectedSession.fireSessionClosedEvent() + it.toList + verify(sessionsService).terminateAndRemove(session1) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala index dea57e98..09c9e8dd 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala @@ -7,6 +7,7 @@ trait UiElement extends AnyElement: def key: String def withKey(key: String): This + def findKey(key: String): UiElement = flat.find(_.key == key).get /** @return * this element along all it's children flattened diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/components/UiElementTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/components/UiElementTest.scala new file mode 100644 index 00000000..fa6c2711 --- /dev/null +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/components/UiElementTest.scala @@ -0,0 +1,14 @@ +package org.terminal21.client.components + +import org.scalatest.funsuite.AnyFunSuiteLike +import org.terminal21.client.components.chakra.{Box, Text} +import org.scalatest.matchers.should.Matchers.* + +class UiElementTest extends AnyFunSuiteLike: + test("flat"): + val box = Box(key = "k1").withChildren(Text(key = "k2"), Text(key = "k3")) + box.flat should be( + Seq(box, Text(key = "k2"), Text(key = "k3")) + ) + test("findKey"): + Box(key = "k1").withChildren(Text(key = "k2"), Text(key = "k3")).findKey("k3") should be(Text(key = "k3")) From afabe92ec88a32199c21b8aa724c0f2d54d88157 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 28 Feb 2024 12:20:20 +0000 Subject: [PATCH 204/313] - --- .../serverapp/bundled/SettingsPageTest.scala | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/SettingsPageTest.scala b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/SettingsPageTest.scala index 54910226..a422c5d6 100644 --- a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/SettingsPageTest.scala +++ b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/SettingsPageTest.scala @@ -1,14 +1,14 @@ -//package org.terminal21.serverapp.bundled -// -//import org.scalatest.funsuite.AnyFunSuiteLike -//import org.scalatest.matchers.should.Matchers.* -//import org.terminal21.client.{*, given} -// -//class SettingsPageTest extends AnyFunSuiteLike: -// class App: -// given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock -// val page = new SettingsPage -// -// test("Should render the ThemeToggle component"): -// new App: -// page.components should contain(page.themeToggle) +package org.terminal21.serverapp.bundled + +import org.scalatest.funsuite.AnyFunSuiteLike +import org.scalatest.matchers.should.Matchers.* +import org.terminal21.client.{*, given} + +class SettingsPageTest extends AnyFunSuiteLike: + class App: + given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock + val page = new SettingsPage + + test("Should render the ThemeToggle component"): + new App: + page.components(()) should contain(page.themeToggle) From 22f6cfe3550934df22d91ad821e64f7dc272c437 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 28 Feb 2024 14:04:21 +0000 Subject: [PATCH 205/313] - --- .../main/scala/tests/ChakraComponents.scala | 25 +-- .../{LoginForm.scala => LoginPage.scala} | 62 ++++---- .../src/main/scala/tests/StdComponents.scala | 59 +++---- .../src/main/scala/tests/chakra/Buttons.scala | 13 +- .../main/scala/tests/chakra/ChakraModel.scala | 11 ++ .../main/scala/tests/chakra/Editables.scala | 35 ++--- .../src/main/scala/tests/chakra/Forms.scala | 144 ++++++++---------- .../src/main/scala/tests/chakra/Grids.scala | 2 +- .../main/scala/tests/chakra/Navigation.scala | 18 +-- .../src/main/scala/tests/chakra/Overlay.scala | 22 +-- .../src/test/scala/tests/LoggedInTest.scala | 2 +- .../src/test/scala/tests/LoginFormTest.scala | 54 ------- .../src/test/scala/tests/LoginPageTest.scala | 46 ++++++ .../org/terminal21/client/Controller.scala | 3 + 14 files changed, 249 insertions(+), 247 deletions(-) rename end-to-end-tests/src/main/scala/tests/{LoginForm.scala => LoginPage.scala} (59%) create mode 100644 end-to-end-tests/src/main/scala/tests/chakra/ChakraModel.scala delete mode 100644 end-to-end-tests/src/test/scala/tests/LoginFormTest.scala create mode 100644 end-to-end-tests/src/test/scala/tests/LoginPageTest.scala diff --git a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala index 3b211caf..4782a544 100644 --- a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala @@ -12,22 +12,29 @@ import tests.chakra.* Sessions .withNewSession("chakra-components", "Chakra Components") .connect: session => - given ConnectedSession = session - given model: Model[Boolean] = Model(false) + given ConnectedSession = session + given model: Model[ChakraModel] = Model(ChakraModel()) // react tests reset the session to clear state val krButton = Button(text = "Reset state").onClick: event => - event.handled.withModel(true).terminate + event.handled.withModel(_.copy(rerun = true)).terminate - val components: Seq[UiElement] = - Overlay.components ++ Forms.components ++ Editables.components ++ Stacks.components ++ Grids.components ++ Buttons.components ++ Etc.components ++ MediaAndIcons.components ++ DataDisplay.components ++ Typography.components ++ Feedback.components ++ Disclosure.components ++ Navigation.components ++ Seq( - krButton - ) + def components(m: ChakraModel): Seq[UiElement] = + Overlay.components( + m + ) ++ Forms.components( + m + ) ++ Editables.components( + m + ) ++ Stacks.components ++ Grids.components ++ Buttons.components ++ Etc.components ++ MediaAndIcons.components ++ DataDisplay.components ++ Typography.components ++ Feedback.components ++ Disclosure.components ++ + Navigation.components(m) ++ Seq( + krButton + ) Controller(components).render().handledEventsIterator.lastOption.map(_.model) match - case Some(true) => + case Some(m) if m.rerun => session.render(Seq(Paragraph(text = "chakra-session-reset"))) Thread.sleep(500) loop() - case _ => + case _ => loop() diff --git a/end-to-end-tests/src/main/scala/tests/LoginForm.scala b/end-to-end-tests/src/main/scala/tests/LoginPage.scala similarity index 59% rename from end-to-end-tests/src/main/scala/tests/LoginForm.scala rename to end-to-end-tests/src/main/scala/tests/LoginPage.scala index d1bda27d..f1902055 100644 --- a/end-to-end-tests/src/main/scala/tests/LoginForm.scala +++ b/end-to-end-tests/src/main/scala/tests/LoginPage.scala @@ -11,43 +11,52 @@ import org.terminal21.client.* .connect: session => given ConnectedSession = session val confirmed = for - login <- new LoginForm().run() + login <- new LoginPage().run() isYes <- new LoggedIn(login).run() yield isYes if confirmed.getOrElse(false) then println("User confirmed the details") else println("Not confirmed") -case class Login(email: String, pwd: String): +case class LoginForm(email: String = "my@email.com", pwd: String = "mysecret", submitted: Boolean = false, submittedInvalidEmail: Boolean = false): def isValidEmail: Boolean = email.contains("@") /** The login form. Displays an email and password input and a submit button. When run() it will fill in the Login(email,pwd) model. */ -class LoginForm(using session: ConnectedSession): - private given initialModel: Model[Login] = Model(Login("my@email.com", "mysecret")) - val okIcon = CheckCircleIcon(color = Some("green")) - val notOkIcon = WarningTwoIcon(color = Some("red")) - val emailRightAddon = InputRightAddon().withChildren(okIcon) - val emailInput = Input(`type` = "email", defaultValue = initialModel.value.email) +class LoginPage(using session: ConnectedSession): + private given initialModel: Model[LoginForm] = Model(LoginForm()) + val okIcon = CheckCircleIcon(color = Some("green")) + val notOkIcon = WarningTwoIcon(color = Some("red")) + val emailInput = Input(key = "email", `type` = "email", defaultValue = initialModel.value.email) .onChange: changeEvent => - changeEvent.handled.withRenderChanges(validate(changeEvent.model)) + import changeEvent.* + handled.withModel(model.copy(email = newValue)) - val submitButton = Button(text = "Submit") + val submitButton = Button(key = "submit", text = "Submit") .onClick: clickEvent => import clickEvent.* // if the email is invalid, we will not terminate. We also will render an error that will be visible for 2 seconds val isValidEmail = model.isValidEmail - val messageBox = - if isValidEmail then errorsBox.current else errorsBox.current.addChildren(errorMsgInvalidEmail) - handled.withShouldTerminate(isValidEmail).withRenderChanges(messageBox).addTimedRenderChange(2000, errorsBox) + handled.withModel(_.copy(submitted = isValidEmail, submittedInvalidEmail = !isValidEmail)) + + val passwordInput = Input(key = "password", `type` = "password", defaultValue = initialModel.value.pwd) + .onChange: changeEvent => + import changeEvent.* + handled.withModel(model.copy(pwd = newValue)) - val passwordInput = Input(`type` = "password", defaultValue = initialModel.value.pwd) val errorsBox = Box() val errorMsgInvalidEmail = Paragraph(text = "Invalid Email", style = Map("color" -> "red")) - def run(): Option[Login] = - controller.render().handledEventsIterator.lastOptionOrNoneIfSessionClosed.map(_.model) - - def components: Seq[UiElement] = + def run(): Option[LoginForm] = + controller + .render() + .handledEventsIterator + .map(_.model) + .tapEach: form => + println(form) + .dropWhile(!_.submitted) + .nextOption() + + def components(loginForm: LoginForm): Seq[UiElement] = Seq( QuickFormControl() .withLabel("Email address") @@ -55,7 +64,7 @@ class LoginForm(using session: ConnectedSession): .withInputGroup( InputLeftAddon().withChildren(EmailIcon()), emailInput, - emailRightAddon + InputRightAddon().withChildren(if loginForm.isValidEmail then okIcon else notOkIcon) ), QuickFormControl() .withLabel("Password") @@ -65,19 +74,16 @@ class LoginForm(using session: ConnectedSession): passwordInput ), submitButton, - errorsBox + if loginForm.submittedInvalidEmail then errorsBox.withChildren(errorMsgInvalidEmail) else errorsBox ) - def controller: Controller[Login] = Controller(components) + def controller: Controller[LoginForm] = Controller(components) .onEvent: event => import event.* - val newModel = event.model.copy(email = emailInput.current.value, pwd = passwordInput.current.value) - event.handled.withModel(newModel) - - private def validate(login: Login): InputRightAddon = - if login.isValidEmail then emailRightAddon.withChildren(okIcon) else emailRightAddon.withChildren(notOkIcon) + val newModel = model.copy(submittedInvalidEmail = false) + handled.withModel(newModel) -class LoggedIn(login: Login)(using session: ConnectedSession): +class LoggedIn(login: LoginForm)(using session: ConnectedSession): private given Model[Boolean] = Model(false) val yesButton = Button(text = "Yes") .onClick: e => @@ -93,7 +99,7 @@ class LoggedIn(login: Login)(using session: ConnectedSession): def run(): Option[Boolean] = controller.render().handledEventsIterator.lastOption.map(_.model) - def components = + def components: Seq[UiElement] = Seq( Paragraph().withChildren( Text(text = "Are your details correct?"), diff --git a/end-to-end-tests/src/main/scala/tests/StdComponents.scala b/end-to-end-tests/src/main/scala/tests/StdComponents.scala index 6eca9052..55fdcce1 100644 --- a/end-to-end-tests/src/main/scala/tests/StdComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/StdComponents.scala @@ -9,36 +9,39 @@ import org.terminal21.client.components.std.* .withNewSession("std-components", "Std Components") .connect: session => given ConnectedSession = session - import Model.Standard.unitModel - val output = Paragraph(text = "This will reflect what you type in the input") - val cookieValue = Paragraph(text = "This will display the value of the cookie") - val input = Input(defaultValue = "Please enter your name").onChange: event => - import event.* - handled.withRenderChanges(output.withText(newValue)) + case class Form(output: String, cookie: String) + given Model[Form] = Model(Form("This will reflect what you type in the input", "This will display the value of the cookie")) - val components = Seq( - Header1(text = "header1 test"), - Header2(text = "header2 test"), - Header3(text = "header3 test"), - Header4(text = "header4 test"), - Header5(text = "header5 test"), - Header6(text = "header6 test"), - Paragraph(text = "Hello World!").withChildren( - NewLine(), - Span(text = "Some more text"), - Em(text = " emphasized!"), - NewLine(), - Span(text = "And the last line") - ), - Paragraph(text = "A Form").withChildren(input), - output, - Cookie(name = "std-components-test-cookie", value = "test-cookie-value"), - CookieReader(name = "std-components-test-cookie").onChange: event => + def components(form: Form) = + val output = Paragraph(text = form.output) + val cookieValue = Paragraph(text = form.cookie) + val input = Input(defaultValue = "Please enter your name").onChange: event => import event.* - handled.withRenderChanges(cookieValue.withText(s"Cookie value $newValue")) - , - cookieValue - ) + handled.withModel(form.copy(output = newValue)) + + Seq( + Header1(text = "header1 test"), + Header2(text = "header2 test"), + Header3(text = "header3 test"), + Header4(text = "header4 test"), + Header5(text = "header5 test"), + Header6(text = "header6 test"), + Paragraph(text = "Hello World!").withChildren( + NewLine(), + Span(text = "Some more text"), + Em(text = " emphasized!"), + NewLine(), + Span(text = "And the last line") + ), + Paragraph(text = "A Form").withChildren(input), + output, + Cookie(name = "std-components-test-cookie", value = "test-cookie-value"), + CookieReader(name = "std-components-test-cookie").onChange: event => + import event.* + handled.withModel(_.copy(cookie = s"Cookie value $newValue")) + , + cookieValue + ) Controller(components).render().handledEventsIterator.lastOption diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Buttons.scala b/end-to-end-tests/src/main/scala/tests/chakra/Buttons.scala index beb70126..8a83044a 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Buttons.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Buttons.scala @@ -8,16 +8,11 @@ import tests.chakra.Common.* import java.util.concurrent.CountDownLatch object Buttons: - def components(using Model[Boolean]): Seq[UiElement] = + def components(using Model[ChakraModel]): Seq[UiElement] = val box1 = commonBox(text = "Buttons") - val exitButton = Button(text = "Click to exit program", colorScheme = Some("red")) + val exitButton = Button(key = "exit-button", text = "Click to exit program", colorScheme = Some("red")).onClick: event => + event.handled.terminate Seq( box1, - exitButton.onClick: event => - event.handled - .withRenderChanges( - box1.withText("Exit Clicked!"), - exitButton.withText("Stopping...").withColorScheme(Some("green")) - ) - .terminate + exitButton ) diff --git a/end-to-end-tests/src/main/scala/tests/chakra/ChakraModel.scala b/end-to-end-tests/src/main/scala/tests/chakra/ChakraModel.scala new file mode 100644 index 00000000..08ab223b --- /dev/null +++ b/end-to-end-tests/src/main/scala/tests/chakra/ChakraModel.scala @@ -0,0 +1,11 @@ +package tests.chakra + +case class ChakraModel( + rerun: Boolean = false, + box1: String = "Clicks will be reported here.", + editableStatus: String = "This will reflect any changes in the form.", + formStatus: String = "This will reflect any changes in the form.", + email: String = "the-test-email@email.com", + breadcrumbStatus: String = "no-breadcrumb-clicked", + linkStatus: String = "no-link-clicked" +) diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala b/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala index c23cf651..2b41f34a 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala @@ -6,25 +6,26 @@ import org.terminal21.client.components.chakra.* import tests.chakra.Common.* object Editables: - def components(using ConnectedSession, Model[Boolean]): Seq[UiElement] = - val status = Box(text = "This will reflect any changes in the form.") + def components(model: ChakraModel)(using Model[ChakraModel]): Seq[UiElement] = + val status = Box(text = model.editableStatus) - val editable1I = Editable(defaultValue = "Please type here").withChildren( - EditablePreview(), - EditableInput() - ) - - val editable1 = editable1I.onChange: event => - import event.* - handled.withRenderChanges(status.withText(s"editable1 newValue = $newValue, verify editable1.value = ${editable1I.current.value}")) + val editable1 = Editable(key = "editable1", defaultValue = "Please type here") + .withChildren( + EditablePreview(), + EditableInput() + ) + .onChange: event => + import event.* + handled.withModel(_.copy(editableStatus = s"editable1 newValue = $newValue")) - val editable2I = Editable(defaultValue = "For longer maybe-editable texts\nUse an EditableTextarea\nIt uses a textarea control.").withChildren( - EditablePreview(), - EditableTextarea() - ) - val editable2 = editable2I.onChange: event => - import event.* - handled.withRenderChanges(status.withText(s"editable2 newValue = $newValue, verify editable2.value = ${editable2I.current.value}")) + val editable2 = Editable(key = "editable2", defaultValue = "For longer maybe-editable texts\nUse an EditableTextarea\nIt uses a textarea control.") + .withChildren( + EditablePreview(), + EditableTextarea() + ) + .onChange: event => + import event.* + handled.withModel(_.copy(editableStatus = s"editable2 newValue = $newValue")) Seq( commonBox(text = "Editables"), diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala b/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala index 6fba0202..4b1d51cd 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala @@ -6,87 +6,76 @@ import org.terminal21.client.components.chakra.* import tests.chakra.Common.* object Forms: - def components(using Model[Boolean]): Seq[UiElement] = - val status = Box(text = "This will reflect any changes in the form.") + def components(m: ChakraModel)(using Model[ChakraModel]): Seq[UiElement] = + val status = Box(text = m.formStatus) val okIcon = CheckCircleIcon(color = Some("green")) val notOkIcon = WarningTwoIcon(color = Some("red")) - val emailRightAddOn = InputRightAddon().withChildren(okIcon) + val emailRightAddOn = InputRightAddon().withChildren(if m.email.contains("@") then okIcon else notOkIcon) - val emailI = Input(`type` = "email", defaultValue = "the-test-email@email.com") - val email = emailI.onChange: event => - import event.* - handled.withRenderChanges( - status.withText(s"email input new value = $newValue, verify email.value = ${emailI.current.value}"), - if newValue.contains("@") then emailRightAddOn.withChildren(okIcon) else emailRightAddOn.withChildren(notOkIcon) - ) - - val descriptionI = Textarea(placeholder = "Please enter a few things about you", defaultValue = "desc") - val description = descriptionI.onChange: event => - import event.* - handled.withRenderChanges(status.withText(s"description input new value = $newValue, verify description.value = ${descriptionI.current.value}")) - - val select1I = Select(placeholder = "Please choose").withChildren( - Option_(text = "Male", value = "male"), - Option_(text = "Female", value = "female") - ) - - val select1 = select1I.onChange: event => - import event.* - handled.withRenderChanges(status.withText(s"select1 input new value = $newValue, verify select1.value = ${select1I.current.value}")) - - val select2 = Select(defaultValue = "1", bg = Some("tomato"), color = Some("black"), borderColor = Some("yellow")).withChildren( - Option_(text = "First", value = "1"), - Option_(text = "Second", value = "2") - ) - - val password = Input(`type` = "password", defaultValue = "mysecret") - val dobI = Input(`type` = "datetime-local") - val dob = dobI.onChange: event => - import event.* - handled.withRenderChanges(status.withText(s"dob = $newValue , verify dob.value = ${dobI.current.value}")) + val email = Input(key = "email", `type` = "email", defaultValue = m.email) + .onChange: event => + import event.* + handled.withModel(_.copy(email = newValue, formStatus = s"email input new value = $newValue")) - val colorI = Input(`type` = "color") + val description = Textarea(key = "textarea", placeholder = "Please enter a few things about you", defaultValue = "desc") + .onChange: event => + import event.* + handled.withModel(_.copy(formStatus = s"description input new value = $newValue")) - val color = colorI.onChange: event => - import event.* - handled.withRenderChanges(status.withText(s"color = $newValue , verify color.value = ${colorI.current.value}")) - - val checkbox2I = Checkbox(text = "Check 2", defaultChecked = true) - val checkbox2 = checkbox2I.onChange: event => - import event.* - handled.withRenderChanges(status.withText(s"checkbox2 checked is $newValue , verify checkbox2.checked = ${checkbox2I.current.checked}")) - - val checkbox1I = Checkbox(text = "Check 1") - val checkbox1 = checkbox1I.onChange: event => - import event.* - handled.withRenderChanges( - status.withText(s"checkbox1 checked is $newValue , verify checkbox1.checked = ${checkbox1I.current.checked}"), - checkbox2.withIsDisabled(newValue) + val select1 = Select(key = "male/female", placeholder = "Please choose") + .withChildren( + Option_(text = "Male", value = "male"), + Option_(text = "Female", value = "female") + ) + .onChange: event => + import event.* + handled.withModel(_.copy(formStatus = s"select1 input new value = $newValue")) + + val select2 = + Select(key = "select-first-second", defaultValue = "1", bg = Some("tomato"), color = Some("black"), borderColor = Some("yellow")).withChildren( + Option_(text = "First", value = "1"), + Option_(text = "Second", value = "2") ) - val switch1I = Switch(text = "Switch 1") - val switch2 = Switch(text = "Switch 2", defaultChecked = true) - - val switch1 = switch1I.onChange: event => - import event.* - handled - .withRenderChanges( - status.withText(s"switch1 checked is $newValue , verify switch1.checked = ${switch1I.current.checked}"), - switch2.withIsDisabled(newValue) + val password = Input(key = "password", `type` = "password", defaultValue = "mysecret") + val dob = Input(key = "dob", `type` = "datetime-local") + .onChange: event => + import event.* + handled.withModel(_.copy(formStatus = s"dob = $newValue")) + + val color = Input(key = "color", `type` = "color") + .onChange: event => + import event.* + handled.withModel(_.copy(formStatus = s"color = $newValue")) + + val checkbox2 = Checkbox(key = "cb2", text = "Check 2", defaultChecked = true) + .onChange: event => + import event.* + handled.withModel(_.copy(formStatus = s"checkbox2 checked is $newValue")) + + val checkbox1 = Checkbox(key = "cb1", text = "Check 1") + .onChange: event => + import event.* + handled.withModel(_.copy(formStatus = s"checkbox1 checked is $newValue")) + + val switch1 = Switch(key = "sw1", text = "Switch 1") + .onChange: event => + import event.* + handled.withModel(_.copy(formStatus = s"switch1 checked is $newValue")) + val switch2 = Switch(key = "sw2", text = "Switch 2", defaultChecked = true) + + val radioGroup = RadioGroup(key = "radio", defaultValue = "2") + .withChildren( + HStack().withChildren( + Radio(value = "1", text = "first"), + Radio(value = "2", text = "second"), + Radio(value = "3", text = "third") ) - - val radioGroupI = RadioGroup(defaultValue = "2").withChildren( - HStack().withChildren( - Radio(value = "1", text = "first"), - Radio(value = "2", text = "second"), - Radio(value = "3", text = "third") ) - ) - - val radioGroup = radioGroupI.onChange: event => - import event.* - handled.withRenderChanges(status.withText(s"radioGroup newValue=$newValue , verify radioGroup.value=${radioGroupI.current.value}")) + .onChange: event => + import event.* + handled.withModel(_.copy(formStatus = s"radioGroup newValue=$newValue")) Seq( commonBox(text = "Forms"), @@ -142,20 +131,15 @@ object Forms: switch2 ), ButtonGroup(variant = Some("outline"), spacing = Some("24")).withChildren( - Button(text = "Save", colorScheme = Some("red")) + Button(key = "save-button", text = "Save", colorScheme = Some("red")) .onClick: event => import event.* - handled.withRenderChanges( - status - .withText( - s"Saved clicked. Email = ${email.current.value}, password = ${password.current.value}, dob = ${dob.current.value}, check1 = ${checkbox1.current.checked}, check2 = ${checkbox2.current.checked}, radio = ${radioGroup.current.value}, switch1 = ${switch1.current.checked}, switch2 = ${switch2.current.checked}" - ) - ) + handled.withModel(_.copy(formStatus = s"Saved clicked")) , - Button(text = "Cancel") + Button(key = "cancel-button", text = "Cancel") .onClick: event => import event.* - handled.withRenderChanges(status.withText("Cancel clicked")) + handled.withModel(_.copy(formStatus = s"Cancel clicked")) ), radioGroup, status diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Grids.scala b/end-to-end-tests/src/main/scala/tests/chakra/Grids.scala index cff12e77..afe9e87a 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Grids.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Grids.scala @@ -6,7 +6,7 @@ import org.terminal21.client.components.chakra.{Box, SimpleGrid} import tests.chakra.Common.* object Grids: - def components(using session: ConnectedSession): Seq[UiElement] = + def components: Seq[UiElement] = val box1 = commonBox(text = "Simple grid") Seq( box1, diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Navigation.scala b/end-to-end-tests/src/main/scala/tests/chakra/Navigation.scala index 6ed38edc..0dd55c1e 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Navigation.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Navigation.scala @@ -7,11 +7,11 @@ import org.terminal21.client.components.std.Paragraph import tests.chakra.Common.commonBox object Navigation: - def components(using Model[Boolean]): Seq[UiElement] = - val clickedBreadcrumb = Paragraph(text = "no-breadcrumb-clicked") - def breadcrumbClicked(t: String) = clickedBreadcrumb.withText(s"breadcrumb-click: $t") + def components(m: ChakraModel)(using Model[ChakraModel]): Seq[UiElement] = + val clickedBreadcrumb = Paragraph(text = m.breadcrumbStatus) + def breadcrumbClicked(m: ChakraModel, t: String) = m.copy(breadcrumbStatus = s"breadcrumb-click: $t") - val clickedLink = Paragraph(text = "no-link-clicked") + val clickedLink = Paragraph(text = m.linkStatus) Seq( commonBox(text = "Breadcrumbs"), @@ -19,25 +19,25 @@ object Navigation: BreadcrumbItem().withChildren( BreadcrumbLink(text = "breadcrumb-home").onClick: event => import event.* - handled.withRenderChanges(breadcrumbClicked("breadcrumb-home")) + handled.withModel(breadcrumbClicked(model, "breadcrumb-home")) ), BreadcrumbItem().withChildren( BreadcrumbLink(text = "breadcrumb-link1").onClick: event => import event.* - handled.withRenderChanges(breadcrumbClicked("breadcrumb-link1")) + handled.withModel(breadcrumbClicked(model, "breadcrumb-link1")) ), BreadcrumbItem(isCurrentPage = Some(true)).withChildren( BreadcrumbLink(text = "breadcrumb-link2").onClick: event => import event.* - handled.withRenderChanges(breadcrumbClicked("breadcrumb-link2")) + handled.withModel(breadcrumbClicked(model, "breadcrumb-link2")) ) ), clickedBreadcrumb, commonBox(text = "Link"), - Link(text = "link-external-google", href = "https://www.google.com/", isExternal = Some(true)) + Link(key = "google-link", text = "link-external-google", href = "https://www.google.com/", isExternal = Some(true)) .onClick: event => import event.* - handled.withRenderChanges(clickedLink.withText("link-clicked")) + handled.withModel(_.copy(linkStatus = "link-clicked")) , clickedLink ) diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala b/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala index f4f51d77..dc1e06d5 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala @@ -6,33 +6,33 @@ import org.terminal21.client.components.chakra.* import tests.chakra.Common.commonBox object Overlay: - def components(using Model[Boolean]): Seq[UiElement] = - val box1 = Box(text = "Clicks will be reported here.") + def components(m: ChakraModel)(using Model[ChakraModel]): Seq[UiElement] = + val box1 = Box(text = m.box1) Seq( commonBox(text = "Menus box0001"), HStack().withChildren( - Menu().withChildren( + Menu(key = "menu1").withChildren( MenuButton(text = "Actions menu0001", size = Some("sm"), colorScheme = Some("teal")).withChildren( ChevronDownIcon() ), MenuList().withChildren( - MenuItem(text = "Download menu-download") + MenuItem(key = "download-menu", text = "Download menu-download") .onClick: event => import event.* - handled.withRenderChanges(box1.withText("'Download' clicked")) + handled.withModel(_.copy(box1 = "'Download' clicked")) , - MenuItem(text = "Copy").onClick: event => + MenuItem(key = "copy-menu", text = "Copy").onClick: event => import event.* - handled.withRenderChanges(box1.withText("'Copy' clicked")) + handled.withModel(_.copy(box1 = "'Copy' clicked")) , - MenuItem(text = "Paste").onClick: event => + MenuItem(key = "paste-menu", text = "Paste").onClick: event => import event.* - handled.withRenderChanges(box1.withText("'Paste' clicked")) + handled.withModel(_.copy(box1 = "'Paste' clicked")) , MenuDivider(), - MenuItem(text = "Exit").onClick: event => + MenuItem(key = "exit-menu", text = "Exit").onClick: event => import event.* - handled.withRenderChanges(box1.withText("'Exit' clicked")) + handled.withModel(_.copy(box1 = "'Exit' clicked")) ) ), box1 diff --git a/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala b/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala index e7806a45..c6f2a1a1 100644 --- a/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala +++ b/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala @@ -7,7 +7,7 @@ import org.terminal21.model.CommandEvent class LoggedInTest extends AnyFunSuiteLike: class App: - val login = Login("my@email.com", "secret") + val login = LoginForm() given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock val form = new LoggedIn(login) def allComponents = form.components.flatMap(_.flat) diff --git a/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala b/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala deleted file mode 100644 index 13b93ac1..00000000 --- a/end-to-end-tests/src/test/scala/tests/LoginFormTest.scala +++ /dev/null @@ -1,54 +0,0 @@ -package tests - -import org.scalatest.funsuite.AnyFunSuiteLike -import org.scalatest.matchers.should.Matchers.* -import org.terminal21.client.components.* -import org.terminal21.client.{ConnectedSession, ConnectedSessionMock} -import org.terminal21.model.CommandEvent - -class LoginFormTest extends AnyFunSuiteLike: - - class App: - given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock - val form = new LoginForm(using session) - def allComponents = form.components.flatMap(_.flat) - - test("renders email input"): - new App: - allComponents should contain(form.emailInput) - - test("renders password input"): - new App: - allComponents should contain(form.passwordInput) - - test("renders submit button"): - new App: - allComponents should contain(form.submitButton) - - test("user submits validated data"): - new App: - val eventsIt = form.controller.render().handledEventsIterator // get the iterator before we fire the events, otherwise the iterator will be empty - session.fireEvents( - CommandEvent.onChange(form.emailInput, "an@email.com"), - CommandEvent.onChange(form.passwordInput, "secret"), - CommandEvent.onClick(form.submitButton), - CommandEvent.sessionClosed // every test should close the session so that the iterator doesn't block if converted to a list. - ) - - eventsIt.lastOption.map(_.model) should be(Some(Login("an@email.com", "secret"))) - - test("user submits invalid email"): - new App: - val eventsIt = form.controller.render().handledEventsIterator // get the iterator that iterates Handled instances so that we can assert on renderChanges - session.fireEvents( - CommandEvent.onChange(form.emailInput, "invalid-email.com"), - CommandEvent.onClick(form.submitButton), - CommandEvent.sessionClosed - ) - val allHandled = eventsIt.toList - // the event processing shouldn't have terminated because of the email error - allHandled.exists(_.shouldTerminate) should be(false) - // the email right addon should have rendered with the notOkIcon when the user typed the incorrect email - allHandled(1).renderChanges should be(Seq(form.emailRightAddon.withChildren(form.notOkIcon))) - // An error message in the errorsBox should be displayed when the user clicked on the submit - allHandled(2).renderChanges should be(Seq(form.errorsBox.withChildren(form.errorMsgInvalidEmail))) diff --git a/end-to-end-tests/src/test/scala/tests/LoginPageTest.scala b/end-to-end-tests/src/test/scala/tests/LoginPageTest.scala new file mode 100644 index 00000000..ec8f7a1a --- /dev/null +++ b/end-to-end-tests/src/test/scala/tests/LoginPageTest.scala @@ -0,0 +1,46 @@ +package tests + +import org.scalatest.funsuite.AnyFunSuiteLike +import org.scalatest.matchers.should.Matchers.* +import org.terminal21.client.components.* +import org.terminal21.client.{ConnectedSession, ConnectedSessionMock} +import org.terminal21.model.CommandEvent + +class LoginPageTest extends AnyFunSuiteLike: + + class App: + given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock + val login = LoginForm() + val page = new LoginPage + def allComponents: Seq[UiElement] = allComponents(login) + def allComponents(form: LoginForm): Seq[UiElement] = page.components(form).flatMap(_.flat) + + test("renders email input"): + new App: + allComponents should contain(page.emailInput) + + test("renders password input"): + new App: + allComponents should contain(page.passwordInput) + + test("renders submit button"): + new App: + allComponents should contain(page.submitButton) + + test("user submits validated data"): + new App: + val eventsIt = page.controller.render().handledEventsIterator // get the iterator before we fire the events, otherwise the iterator will be empty + session.fireEvents( + CommandEvent.onChange(page.emailInput, "an@email.com"), + CommandEvent.onChange(page.passwordInput, "secret"), + CommandEvent.onClick(page.submitButton), + CommandEvent.sessionClosed // every test should close the session so that the iterator doesn't block if converted to a list. + ) + + eventsIt.lastOption.map(_.model) should be(Some(LoginForm("an@email.com", "secret"))) + + test("user submits invalid email"): + new App: + val all = allComponents(login.copy(email = "invalid.com", submitted = false, submittedInvalidEmail = true)) + all should contain(page.notOkIcon) + all should contain(page.errorsBox.withChildren(page.errorMsgInvalidEmail)) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 8aa06e21..96b83879 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -148,6 +148,8 @@ object Controller: new Controller(session.eventIterator, session.renderChanges, modelComponents, initialModel, Nil) def apply[M](modelComponents: M => Seq[UiElement])(using initialModel: Model[M], session: ConnectedSession): Controller[M] = new Controller(session.eventIterator, session.renderChanges, modelComponents, initialModel, Nil) + def apply[M](modelComponents: Seq[UiElement])(using initialModel: Model[M], session: ConnectedSession): Controller[M] = + new Controller(session.eventIterator, session.renderChanges, _ => modelComponents, initialModel, Nil) sealed trait ControllerEvent[M]: def model: M = handled.model @@ -166,6 +168,7 @@ case class HandledEvent[M]( def terminate: HandledEvent[M] = copy(shouldTerminate = true) def withShouldTerminate(t: Boolean): HandledEvent[M] = copy(shouldTerminate = t) def withModel(m: M): HandledEvent[M] = copy(model = m) + def withModel(f: M => M): HandledEvent[M] = copy(model = f(model)) type OnClickEventHandlerFunction[M] = ControllerClickEvent[M] => HandledEvent[M] type OnChangeEventHandlerFunction[M] = ControllerChangeEvent[M] => HandledEvent[M] From 68046195a1b897f0b0fa50922911b3d9a45a6278 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 28 Feb 2024 14:42:49 +0000 Subject: [PATCH 206/313] - --- end-to-end-tests/src/test/scala/tests/LoginPageTest.scala | 2 +- .../org/terminal21/client/components/UiComponent.scala | 4 ++-- .../client/components/chakra/QuickFormControl.scala | 8 ++++---- .../terminal21/client/components/chakra/QuickTable.scala | 8 ++++---- .../terminal21/client/components/chakra/QuickTabs.scala | 6 +++--- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/end-to-end-tests/src/test/scala/tests/LoginPageTest.scala b/end-to-end-tests/src/test/scala/tests/LoginPageTest.scala index ec8f7a1a..090ffb30 100644 --- a/end-to-end-tests/src/test/scala/tests/LoginPageTest.scala +++ b/end-to-end-tests/src/test/scala/tests/LoginPageTest.scala @@ -37,7 +37,7 @@ class LoginPageTest extends AnyFunSuiteLike: CommandEvent.sessionClosed // every test should close the session so that the iterator doesn't block if converted to a list. ) - eventsIt.lastOption.map(_.model) should be(Some(LoginForm("an@email.com", "secret"))) + eventsIt.lastOption.map(_.model) should be(Some(LoginForm("an@email.com", "secret", true))) test("user submits invalid email"): new App: diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiComponent.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiComponent.scala index c1213662..483f21a3 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiComponent.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiComponent.scala @@ -1,7 +1,5 @@ package org.terminal21.client.components -import org.terminal21.client.components.UiElement.HasChildren - /** A UiComponent is a UI element that is composed of a seq of other ui elements */ trait UiComponent extends UiElement: @@ -9,3 +7,5 @@ trait UiComponent extends UiElement: // keys of any sub-elements the component has. def rendered: Seq[UiElement] override def flat = Seq(this) ++ rendered.flatMap(_.flat) + + protected def subKey(suffix: String): String = if key.isEmpty then "" else key + "-" + suffix diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala index 602567a3..2c56f1a2 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala @@ -15,10 +15,10 @@ case class QuickFormControl( type This = QuickFormControl lazy val rendered: Seq[UiElement] = val ch: Seq[UiElement] = - label.map(l => FormLabel(key = key + "-label", text = l)).toSeq ++ - Seq(InputGroup(key = key + "-ig").withChildren(inputGroup: _*)) ++ - helperText.map(h => FormHelperText(key = key + "-helper", text = h)) - linearKeys(key, FormControl(key = key + "-fc", style = style).withChildren(ch: _*)) + label.map(l => FormLabel(key = subKey("-label"), text = l)).toSeq ++ + Seq(InputGroup(key = subKey("-ig")).withChildren(inputGroup*)) ++ + helperText.map(h => FormHelperText(key = subKey("-helper"), text = h)) + linearKeys(key, FormControl(key = subKey("-fc"), style = style).withChildren(ch: _*)) def withLabel(label: String): QuickFormControl = copy(label = Some(label)) def withInputGroup(ig: UiElement*): QuickFormControl = copy(inputGroup = ig) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala index 5a37b43b..76d515ac 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala @@ -24,20 +24,20 @@ case class QuickTable( def withCaption(v: String) = copy(caption = Some(v)) override lazy val rendered: Seq[UiElement] = - val head = Thead(key = key + "-th", children = Seq(Tr(children = headers.map(h => Th(children = Seq(h)))))) + val head = Thead(key = subKey("-th"), children = Seq(Tr(children = headers.map(h => Th(children = Seq(h)))))) val body = Tbody( - key = key + "-tb", + key = subKey("-tb"), children = rows.map: row => Tr(children = row.map(c => Td(children = Seq(c)))) ) val table = Table( - key = key + "-t", + key = subKey("-t"), variant = variant, colorScheme = Some(colorScheme), size = size, children = caption.map(text => TableCaption(text = text)).toSeq ++ Seq(head, body) ) - val tableContainer = TableContainer(key = key + "-tc", style = style, children = Seq(table)) + val tableContainer = TableContainer(key = subKey("-tc"), style = style, children = Seq(table)) linearKeys(key, tableContainer) def withHeaders(headers: String*): QuickTable = copy(headers = headers.map(h => Text(text = h))) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala index c74f8510..d6d3ddcb 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala @@ -19,15 +19,15 @@ case class QuickTabs( override lazy val rendered = linearKeys( key, Seq( - Tabs(key = key + "-tabs", style = style).withChildren( + Tabs(key = subKey("-tabs"), style = style).withChildren( TabList( - key = key + "-tab-list", + key = subKey("-tab-list"), children = tabs.zipWithIndex.map: case (name: String, idx) => Tab(key = s"$key-tab-$idx", text = name) case (elements: Seq[UiElement], idx) => Tab(key = s"$key-tab-$idx", children = elements) ), TabPanels( - key = key + "-panels", + key = subKey("-panels"), children = tabPanels.zipWithIndex.map: (elements, idx) => TabPanel(key = s"$key-panel-$idx", children = elements) ) From c97cf8c9ccbaa39ab3649db05007640843768412 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 28 Feb 2024 16:27:31 +0000 Subject: [PATCH 207/313] - --- build.sbt | 2 -- .../src/main/scala/tests/ChakraComponents.scala | 3 ++- .../src/main/scala/tests/LoginPage.scala | 10 +++++----- .../src/main/scala/tests/MathJaxComponents.scala | 3 +-- .../src/main/scala/tests/NivoComponents.scala | 6 +++--- .../main/scala/tests/StateSessionStateBug.scala | 4 ++-- .../src/test/scala/tests/LoggedInTest.scala | 2 +- .../org/terminal21/client/ConnectedSession.scala | 4 ++-- .../scala/org/terminal21/client/Controller.scala | 13 ++++++++----- .../terminal21/client/components/UiElement.scala | 10 +++++++++- .../components/chakra/QuickFormControl.scala | 5 ++--- .../client/components/chakra/QuickTable.scala | 3 +-- .../client/components/chakra/QuickTabs.scala | 5 +---- .../client/json/UiElementEncoding.scala | 7 ++++--- .../client/components/UiElementTest.scala | 16 +++++++++++++++- 15 files changed, 56 insertions(+), 37 deletions(-) diff --git a/build.sbt b/build.sbt index 94390f14..fff0aa64 100644 --- a/build.sbt +++ b/build.sbt @@ -1,5 +1,3 @@ -import sbt.librarymanagement.ModuleFilter - /** This build has different sections for each integration. I.e. an http4s section and a kafka section. These sections are not related to each other, please * examine the section you're interested in. */ diff --git a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala index 4782a544..cc2cf296 100644 --- a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala @@ -1,5 +1,6 @@ package tests +import cats.conversions.all.autoConvertProfunctorVariance import org.terminal21.client.* import org.terminal21.client.components.UiElement import org.terminal21.client.components.chakra.* @@ -32,7 +33,7 @@ import tests.chakra.* ) Controller(components).render().handledEventsIterator.lastOption.map(_.model) match case Some(m) if m.rerun => - session.render(Seq(Paragraph(text = "chakra-session-reset"))) + Controller.noModel(Seq(Paragraph(text = "chakra-session-reset"))).render() Thread.sleep(500) loop() case _ => diff --git a/end-to-end-tests/src/main/scala/tests/LoginPage.scala b/end-to-end-tests/src/main/scala/tests/LoginPage.scala index f1902055..75a13c9b 100644 --- a/end-to-end-tests/src/main/scala/tests/LoginPage.scala +++ b/end-to-end-tests/src/main/scala/tests/LoginPage.scala @@ -84,12 +84,12 @@ class LoginPage(using session: ConnectedSession): handled.withModel(newModel) class LoggedIn(login: LoginForm)(using session: ConnectedSession): - private given Model[Boolean] = Model(false) - val yesButton = Button(text = "Yes") + import Model.Standard.booleanFalseModel + val yesButton = Button(key = "yes-button", text = "Yes") .onClick: e => e.handled.withModel(true).terminate - val noButton = Button(text = "No") + val noButton = Button(key = "no-button", text = "No") .onClick: e => e.handled.withModel(false).terminate @@ -99,7 +99,7 @@ class LoggedIn(login: LoginForm)(using session: ConnectedSession): def run(): Option[Boolean] = controller.render().handledEventsIterator.lastOption.map(_.model) - def components: Seq[UiElement] = + def components(m: Boolean): Seq[UiElement] = Seq( Paragraph().withChildren( Text(text = "Are your details correct?"), @@ -114,4 +114,4 @@ class LoggedIn(login: LoginForm)(using session: ConnectedSession): /** @return * A controller with a boolean value, true if user clicked "Yes", false for "No" */ - def controller = Controller(components) + def controller: Controller[Boolean] = Controller(components) diff --git a/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala b/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala index dfeb044f..254bc0e4 100644 --- a/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala @@ -11,7 +11,6 @@ import org.terminal21.client.components.mathjax.* .andLibraries(MathJaxLib) .connect: session => given ConnectedSession = session - import Model.Standard.unitModel val components = Seq( HStack().withChildren( @@ -24,5 +23,5 @@ import org.terminal21.client.components.mathjax.* style = Map("backgroundColor" -> "gray") ) ) - Controller(components).render() + Controller.noModel(components).render() session.leaveSessionOpenAfterExiting() diff --git a/end-to-end-tests/src/main/scala/tests/NivoComponents.scala b/end-to-end-tests/src/main/scala/tests/NivoComponents.scala index 35263516..c1a5b689 100644 --- a/end-to-end-tests/src/main/scala/tests/NivoComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/NivoComponents.scala @@ -10,7 +10,7 @@ import tests.nivo.{ResponsiveBarChart, ResponsiveLineChart} .andLibraries(NivoLib) .connect: session => given ConnectedSession = session - import Model.Standard.unitModel - val components = ResponsiveBarChart() ++ ResponsiveLineChart() - Controller(components).render() + + val components = ResponsiveBarChart() ++ ResponsiveLineChart() + Controller.noModel(components).render() session.leaveSessionOpenAfterExiting() diff --git a/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala b/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala index 672ada6c..44b7b52a 100644 --- a/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala +++ b/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala @@ -14,8 +14,8 @@ import java.util.Date given ConnectedSession = session import Model.Standard.unitModel - val date = new Date() - val components = Seq( + val date = new Date() + def components(m: Unit) = Seq( Paragraph(text = s"Now: $date"), QuickTable() .withHeaders("Title", "Value") diff --git a/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala b/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala index c6f2a1a1..85fc992f 100644 --- a/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala +++ b/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala @@ -10,7 +10,7 @@ class LoggedInTest extends AnyFunSuiteLike: val login = LoginForm() given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock val form = new LoggedIn(login) - def allComponents = form.components.flatMap(_.flat) + def allComponents = form.components(false).flatMap(_.flat) test("renders email details"): new App: diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index bf56f026..41c65c77 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -93,7 +93,7 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se * @param es * the UiElements to be rendered. */ - def render(es: Seq[UiElement]): Unit = + private[client] def render(es: Seq[UiElement]): Unit = clear() val j = toJson(es) sessionsService.setSessionJsonState(session, j) @@ -102,7 +102,7 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se * @param es * a seq of updated elements, all these should already have been rendered before (but not necessarily their children) */ - def renderChanges(es: Seq[UiElement]): Unit = + private[client] def renderChanges(es: Seq[UiElement]): Unit = if !isClosed && es.nonEmpty then val j = toJson(es) sessionsService.changeSessionJsonState(session, j) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 96b83879..9d9e2a8f 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -15,10 +15,13 @@ class Controller[M]( initialModel: Model[M], eventHandlers: Seq[PartialFunction[ControllerEvent[M], HandledEvent[M]]] ): + private def prepareComponents(m: M): Seq[UiElement] = + Keys.linearKeys(modelComponents(m).map(_.substituteComponents)) + def render()(using session: ConnectedSession): RenderedController[M] = - val initComponents = Keys.linearKeys(modelComponents(initialModel.value)) + val initComponents = prepareComponents(initialModel.value) session.render(initComponents) - new RenderedController(eventIteratorFactory, initialModel, initComponents, modelComponents, renderChanges, eventHandlers) + new RenderedController(eventIteratorFactory, initialModel, initComponents, prepareComponents, renderChanges, eventHandlers) def onEvent(handler: PartialFunction[ControllerEvent[M], HandledEvent[M]]) = new Controller( @@ -116,7 +119,7 @@ class RenderedController[M]( private def doRenderChanges(oldHandled: HandledEvent[M], newHandled: HandledEvent[M]): HandledEvent[M] = // TODO: optimise what elements are rendered - val all = Keys.linearKeys(modelComponents(newHandled.model)) + val all = modelComponents(newHandled.model) renderChanges(all) newHandled.copy(componentsByKey = calcComponentsByKeyMap(all)) @@ -148,8 +151,8 @@ object Controller: new Controller(session.eventIterator, session.renderChanges, modelComponents, initialModel, Nil) def apply[M](modelComponents: M => Seq[UiElement])(using initialModel: Model[M], session: ConnectedSession): Controller[M] = new Controller(session.eventIterator, session.renderChanges, modelComponents, initialModel, Nil) - def apply[M](modelComponents: Seq[UiElement])(using initialModel: Model[M], session: ConnectedSession): Controller[M] = - new Controller(session.eventIterator, session.renderChanges, _ => modelComponents, initialModel, Nil) + def noModel(modelComponents: Seq[UiElement])(using session: ConnectedSession): Controller[Unit] = + new Controller(session.eventIterator, session.renderChanges, _ => modelComponents, Model.Standard.unitModel, Nil) sealed trait ControllerEvent[M]: def model: M = handled.model diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala index 09c9e8dd..6e458dd0 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala @@ -1,8 +1,10 @@ package org.terminal21.client.components +import org.terminal21.client.components.UiElement.HasChildren +import org.terminal21.client.components.chakra.Box import org.terminal21.collections.{TypedMap, TypedMapKey} -trait UiElement extends AnyElement: +abstract class UiElement extends AnyElement: type This <: UiElement def key: String @@ -14,6 +16,12 @@ trait UiElement extends AnyElement: */ def flat: Seq[UiElement] = Seq(this) + def substituteComponents: UiElement = + this match + case c: UiComponent => Box(key = c.key, text = "", children = c.rendered.map(_.substituteComponents)) + case ch: HasChildren => ch.withChildren(ch.children.map(_.substituteComponents)*) + case _ => this + def toSimpleString: String = s"${getClass.getSimpleName}($key)" object UiElement: diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala index 2c56f1a2..e4f708f4 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala @@ -1,8 +1,7 @@ package org.terminal21.client.components.chakra -import org.terminal21.client.components.Keys.linearKeys -import org.terminal21.client.components.{Keys, UiComponent, UiElement} import org.terminal21.client.components.UiElement.HasStyle +import org.terminal21.client.components.{Keys, UiComponent, UiElement} case class QuickFormControl( key: String = Keys.nextKey, @@ -18,7 +17,7 @@ case class QuickFormControl( label.map(l => FormLabel(key = subKey("-label"), text = l)).toSeq ++ Seq(InputGroup(key = subKey("-ig")).withChildren(inputGroup*)) ++ helperText.map(h => FormHelperText(key = subKey("-helper"), text = h)) - linearKeys(key, FormControl(key = subKey("-fc"), style = style).withChildren(ch: _*)) + Seq(FormControl(key = subKey("-fc"), style = style).withChildren(ch: _*)) def withLabel(label: String): QuickFormControl = copy(label = Some(label)) def withInputGroup(ig: UiElement*): QuickFormControl = copy(inputGroup = ig) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala index 76d515ac..91db62b3 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala @@ -1,6 +1,5 @@ package org.terminal21.client.components.chakra -import org.terminal21.client.components.Keys.linearKeys import org.terminal21.client.components.UiElement.HasStyle import org.terminal21.client.components.{Keys, UiComponent, UiElement} @@ -38,7 +37,7 @@ case class QuickTable( children = caption.map(text => TableCaption(text = text)).toSeq ++ Seq(head, body) ) val tableContainer = TableContainer(key = subKey("-tc"), style = style, children = Seq(table)) - linearKeys(key, tableContainer) + Seq(tableContainer) def withHeaders(headers: String*): QuickTable = copy(headers = headers.map(h => Text(text = h))) def withHeadersElements(headers: UiElement*): QuickTable = copy(headers = headers) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala index d6d3ddcb..2ca87a97 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala @@ -1,6 +1,5 @@ package org.terminal21.client.components.chakra -import org.terminal21.client.components.Keys.linearKeys import org.terminal21.client.components.UiElement.HasStyle import org.terminal21.client.components.{Keys, UiComponent, UiElement} @@ -16,8 +15,7 @@ case class QuickTabs( def withTabs(tabs: String | Seq[UiElement]*): QuickTabs = copy(tabs = tabs) def withTabPanels(tabPanels: Seq[UiElement]*): QuickTabs = copy(tabPanels = tabPanels) - override lazy val rendered = linearKeys( - key, + override lazy val rendered = Seq( Tabs(key = subKey("-tabs"), style = style).withChildren( TabList( @@ -33,7 +31,6 @@ case class QuickTabs( ) ) ) - ) override def withStyle(v: Map[String, Any]): QuickTabs = copy(style = v) override def withKey(key: String): QuickTabs = copy(key = key) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/json/UiElementEncoding.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/json/UiElementEncoding.scala index 864dd040..fc78fce6 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/json/UiElementEncoding.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/json/UiElementEncoding.scala @@ -37,8 +37,9 @@ object StdElementEncoding extends ComponentLib: override def toJson(using Encoder[UiElement]): PartialFunction[UiElement, Json] = case std: StdEJson => std.asJson.mapObject(o => o.add("type", "Std".asJson)) case c: CEJson => c.asJson.mapObject(o => o.add("type", "Chakra".asJson)) - case c: UiComponent => - val b: ChakraElement = Box(key = c.key, text = "") - b.asJson.mapObject(o => o.add("type", "Chakra".asJson)) case std: StdHttp => std.asJson.mapObject(o => o.add("type", "Std".asJson)) case fe: FrontEndElement => fe.asJson.mapObject(o => o.add("type", "FrontEnd".asJson)) + case _: UiComponent => + throw new IllegalStateException("substitute all components before serializing") +// val b: ChakraElement = Box(key = c.key, text = "") +// b.asJson.mapObject(o => o.add("type", "Chakra".asJson)) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/components/UiElementTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/components/UiElementTest.scala index fa6c2711..f8c952c4 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/components/UiElementTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/components/UiElementTest.scala @@ -1,8 +1,9 @@ package org.terminal21.client.components import org.scalatest.funsuite.AnyFunSuiteLike -import org.terminal21.client.components.chakra.{Box, Text} import org.scalatest.matchers.should.Matchers.* +import org.terminal21.client.components.chakra.{Box, QuickTable, Text} +import org.terminal21.client.components.std.Paragraph class UiElementTest extends AnyFunSuiteLike: test("flat"): @@ -12,3 +13,16 @@ class UiElementTest extends AnyFunSuiteLike: ) test("findKey"): Box(key = "k1").withChildren(Text(key = "k2"), Text(key = "k3")).findKey("k3") should be(Text(key = "k3")) + + test("substituteComponents when not component"): + val e = Text() + e.substituteComponents should be(e) + + test("substituteComponents when component"): + val e = QuickTable(key = "k1") + e.substituteComponents should be(Box("k1", children = e.rendered)) + + test("substituteComponents when children are component"): + val t = QuickTable(key = "k1") + val e = Paragraph().withChildren(t) + e.substituteComponents should be(Paragraph().withChildren(Box("k1", children = t.rendered))) From 240c79f98913157057c67a82b9f285cb3829a04a Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 28 Feb 2024 16:31:39 +0000 Subject: [PATCH 208/313] - --- .../org/terminal21/serverapp/bundled/ServerStatusApp.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 97339279..4db24a93 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -131,5 +131,5 @@ class ViewServerStatePage(using session: ConnectedSession): keyTreePanel ) ) - session.render(components) + Controller.noModel(components).render() session.leaveSessionOpenAfterExiting() From f0d19909cf18a653a4084e0d0f90761366822f04 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 28 Feb 2024 16:53:56 +0000 Subject: [PATCH 209/313] - --- .../terminal21/client/ConnectedSessionTest.scala | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala index 7e271748..24f47131 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala @@ -5,7 +5,7 @@ import org.mockito.Mockito.verify import org.scalatest.funsuite.AnyFunSuiteLike import org.scalatest.matchers.should.Matchers.* import org.terminal21.client.ConnectedSessionMock.encoder -import org.terminal21.client.components.chakra.{Button, Checkbox, Editable, Input} +import org.terminal21.client.components.chakra.{Box, Button, Checkbox, Editable, Input} import org.terminal21.client.components.std.{Paragraph, Span} import org.terminal21.model.{CommandEvent, OnChange} import org.terminal21.ui.std.ServerJson @@ -31,16 +31,20 @@ class ConnectedSessionTest extends AnyFunSuiteLike: test("to server json"): val (sessionService, connectedSession) = ConnectedSessionMock.newConnectedSessionAndSessionServiceMock - val p1 = Paragraph(text = "p1") - val span1 = Span(text = "span1") + val p1 = Paragraph(key = "pk", text = "p1") + val span1 = Span(key = "sk", text = "span1") connectedSession.render(Seq(p1.withChildren(span1))) connectedSession.render(Nil) verify(sessionService).setSessionJsonState( connectedSession.session, ServerJson( - Seq(p1.key), - Map(p1.key -> encoder(p1.withChildren()), span1.key -> encoder(span1)), - Map(p1.key -> Seq(span1.key), span1.key -> Nil) + Seq("root"), + Map( + "root" -> encoder(Box("root")).deepDropNullValues, + p1.key -> encoder(p1.withChildren()).deepDropNullValues, + span1.key -> encoder(span1).deepDropNullValues + ), + Map("root" -> List(p1.key), p1.key -> Seq(span1.key), span1.key -> Nil) ) ) From 0088aee8e7d2818a483a51fdfd59ceb1c6a42e41 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 28 Feb 2024 16:55:27 +0000 Subject: [PATCH 210/313] - --- .../terminal21/client/ConnectedSessionTest.scala | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala index 24f47131..a2f8d3a6 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala @@ -51,15 +51,19 @@ class ConnectedSessionTest extends AnyFunSuiteLike: test("renderChanges changes state on server"): val (sessionService, connectedSession) = ConnectedSessionMock.newConnectedSessionAndSessionServiceMock - val p1 = Paragraph(text = "p1") - val span1 = Span(text = "span1") + val p1 = Paragraph(key = "pk", text = "p1") + val span1 = Span(key = "sk", text = "span1") connectedSession.render(Seq(p1)) connectedSession.renderChanges(Seq(p1.withChildren(span1))) verify(sessionService).changeSessionJsonState( connectedSession.session, ServerJson( - Seq(p1.key), - Map(p1.key -> encoder(p1.withChildren()), span1.key -> encoder(span1)), - Map(p1.key -> Seq(span1.key), span1.key -> Nil) + Seq("root"), + Map( + "root" -> encoder(Box("root")).deepDropNullValues, + p1.key -> encoder(p1.withChildren()).deepDropNullValues, + span1.key -> encoder(span1).deepDropNullValues + ), + Map("root" -> List(p1.key), p1.key -> Seq(span1.key), span1.key -> Nil) ) ) From c2a711c629b9c7613d196591c9c174d7e9d03505 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 28 Feb 2024 17:01:56 +0000 Subject: [PATCH 211/313] - --- .../org/terminal21/client/components/EventHandler.scala | 3 +++ .../client/components/chakra/QuickFormControl.scala | 8 ++++---- .../terminal21/client/components/chakra/QuickTable.scala | 8 ++++---- .../terminal21/client/components/chakra/QuickTabs.scala | 6 +++--- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala index c983b270..f9c076ef 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala @@ -8,6 +8,7 @@ trait EventHandler object OnClickEventHandler: trait CanHandleOnClickEvent extends HasDataStore: this: UiElement => + if key.isEmpty then throw new IllegalStateException(s"clickables must have a stable key. Error occurred on $this") def onClick[M](using model: Model[M])(h: OnClickEventHandlerFunction[M]): This = val handlers = dataStore.getOrElse(model.ClickKey, Nil) store(model.ClickKey, handlers :+ h) @@ -15,6 +16,7 @@ object OnClickEventHandler: object OnChangeEventHandler: trait CanHandleOnChangeEvent extends HasDataStore: this: UiElement => + if key.isEmpty then throw new IllegalStateException(s"changeable must have a stable key. Error occurred on $this") def onChange[M](using model: Model[M])(h: OnChangeEventHandlerFunction[M]): This = val handlers = dataStore.getOrElse(model.ChangeKey, Nil) store(model.ChangeKey, handlers :+ h) @@ -22,6 +24,7 @@ object OnChangeEventHandler: object OnChangeBooleanEventHandler: trait CanHandleOnChangeEvent extends HasDataStore: this: UiElement => + if key.isEmpty then throw new IllegalStateException(s"changeable must have a stable key. Error occurred on $this") def onChange[M](using model: Model[M])(h: OnChangeBooleanEventHandlerFunction[M]): This = val handlers = dataStore.getOrElse(model.ChangeBooleanKey, Nil) store(model.ChangeBooleanKey, handlers :+ h) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala index e4f708f4..e31a6b86 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala @@ -14,10 +14,10 @@ case class QuickFormControl( type This = QuickFormControl lazy val rendered: Seq[UiElement] = val ch: Seq[UiElement] = - label.map(l => FormLabel(key = subKey("-label"), text = l)).toSeq ++ - Seq(InputGroup(key = subKey("-ig")).withChildren(inputGroup*)) ++ - helperText.map(h => FormHelperText(key = subKey("-helper"), text = h)) - Seq(FormControl(key = subKey("-fc"), style = style).withChildren(ch: _*)) + label.map(l => FormLabel(key = subKey("label"), text = l)).toSeq ++ + Seq(InputGroup(key = subKey("ig")).withChildren(inputGroup*)) ++ + helperText.map(h => FormHelperText(key = subKey("helper"), text = h)) + Seq(FormControl(key = subKey("fc"), style = style).withChildren(ch: _*)) def withLabel(label: String): QuickFormControl = copy(label = Some(label)) def withInputGroup(ig: UiElement*): QuickFormControl = copy(inputGroup = ig) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala index 91db62b3..8c60425a 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala @@ -23,20 +23,20 @@ case class QuickTable( def withCaption(v: String) = copy(caption = Some(v)) override lazy val rendered: Seq[UiElement] = - val head = Thead(key = subKey("-th"), children = Seq(Tr(children = headers.map(h => Th(children = Seq(h)))))) + val head = Thead(key = subKey("th"), children = Seq(Tr(children = headers.map(h => Th(children = Seq(h)))))) val body = Tbody( - key = subKey("-tb"), + key = subKey("tb"), children = rows.map: row => Tr(children = row.map(c => Td(children = Seq(c)))) ) val table = Table( - key = subKey("-t"), + key = subKey("t"), variant = variant, colorScheme = Some(colorScheme), size = size, children = caption.map(text => TableCaption(text = text)).toSeq ++ Seq(head, body) ) - val tableContainer = TableContainer(key = subKey("-tc"), style = style, children = Seq(table)) + val tableContainer = TableContainer(key = subKey("tc"), style = style, children = Seq(table)) Seq(tableContainer) def withHeaders(headers: String*): QuickTable = copy(headers = headers.map(h => Text(text = h))) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala index 2ca87a97..645a310f 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala @@ -17,15 +17,15 @@ case class QuickTabs( override lazy val rendered = Seq( - Tabs(key = subKey("-tabs"), style = style).withChildren( + Tabs(key = subKey("tabs"), style = style).withChildren( TabList( - key = subKey("-tab-list"), + key = subKey("tab-list"), children = tabs.zipWithIndex.map: case (name: String, idx) => Tab(key = s"$key-tab-$idx", text = name) case (elements: Seq[UiElement], idx) => Tab(key = s"$key-tab-$idx", children = elements) ), TabPanels( - key = subKey("-panels"), + key = subKey("panels"), children = tabPanels.zipWithIndex.map: (elements, idx) => TabPanel(key = s"$key-panel-$idx", children = elements) ) From 603c20462375b07ce9ce8ee3894440a373d7c30f Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 28 Feb 2024 17:09:40 +0000 Subject: [PATCH 212/313] - --- end-to-end-tests/src/main/scala/tests/StdComponents.scala | 4 ++-- .../org/terminal21/serverapp/bundled/ServerStatusApp.scala | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/StdComponents.scala b/end-to-end-tests/src/main/scala/tests/StdComponents.scala index 55fdcce1..d9d55bf6 100644 --- a/end-to-end-tests/src/main/scala/tests/StdComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/StdComponents.scala @@ -16,7 +16,7 @@ import org.terminal21.client.components.std.* def components(form: Form) = val output = Paragraph(text = form.output) val cookieValue = Paragraph(text = form.cookie) - val input = Input(defaultValue = "Please enter your name").onChange: event => + val input = Input(key = "name", defaultValue = "Please enter your name").onChange: event => import event.* handled.withModel(form.copy(output = newValue)) @@ -37,7 +37,7 @@ import org.terminal21.client.components.std.* Paragraph(text = "A Form").withChildren(input), output, Cookie(name = "std-components-test-cookie", value = "test-cookie-value"), - CookieReader(name = "std-components-test-cookie").onChange: event => + CookieReader(key = "cookie-reader", name = "std-components-test-cookie").onChange: event => import event.* handled.withModel(_.copy(cookie = s"Cookie value $newValue")) , diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 4db24a93..345569e0 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -89,7 +89,7 @@ class ServerStatusPage( handled , Text(text = " "), - Button(text = "View State", size = xs) + Button(key = s"view-${session.id}", text = "View State", size = xs) .withLeftIcon(ChatIcon()) .onClick: event => serverSideSessions From 7373fcf7da1f1c69dcba6fd9a47c1298d3e9a0a3 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 28 Feb 2024 17:12:01 +0000 Subject: [PATCH 213/313] - --- .../src/main/scala/tests/ChakraComponents.scala | 3 +-- .../src/main/scala/tests/chakra/DataDisplay.scala | 2 +- .../src/main/scala/tests/chakra/Navigation.scala | 6 +++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala index cc2cf296..38bd5694 100644 --- a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala @@ -1,6 +1,5 @@ package tests -import cats.conversions.all.autoConvertProfunctorVariance import org.terminal21.client.* import org.terminal21.client.components.UiElement import org.terminal21.client.components.chakra.* @@ -17,7 +16,7 @@ import tests.chakra.* given model: Model[ChakraModel] = Model(ChakraModel()) // react tests reset the session to clear state - val krButton = Button(text = "Reset state").onClick: event => + val krButton = Button("reset", text = "Reset state").onClick: event => event.handled.withModel(_.copy(rerun = true)).terminate def components(m: ChakraModel): Seq[UiElement] = diff --git a/end-to-end-tests/src/main/scala/tests/chakra/DataDisplay.scala b/end-to-end-tests/src/main/scala/tests/chakra/DataDisplay.scala index aeba6cbd..2ce70a17 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/DataDisplay.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/DataDisplay.scala @@ -30,7 +30,7 @@ object DataDisplay: Badge(text = "badge 3", size = "lg", colorScheme = Some("green")), Badge(text = "badge 4", variant = Some("outline"), colorScheme = Some("tomato")), Badge(text = "badge 4").withChildren( - Button(text = "test") + Button("test", text = "test") ) ), commonBox(text = "Quick Tables"), diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Navigation.scala b/end-to-end-tests/src/main/scala/tests/chakra/Navigation.scala index 0dd55c1e..ff8efeec 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Navigation.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Navigation.scala @@ -17,17 +17,17 @@ object Navigation: commonBox(text = "Breadcrumbs"), Breadcrumb().withChildren( BreadcrumbItem().withChildren( - BreadcrumbLink(text = "breadcrumb-home").onClick: event => + BreadcrumbLink("breadcrumb-home", text = "breadcrumb-home").onClick: event => import event.* handled.withModel(breadcrumbClicked(model, "breadcrumb-home")) ), BreadcrumbItem().withChildren( - BreadcrumbLink(text = "breadcrumb-link1").onClick: event => + BreadcrumbLink("breadcrumb-link1", text = "breadcrumb-link1").onClick: event => import event.* handled.withModel(breadcrumbClicked(model, "breadcrumb-link1")) ), BreadcrumbItem(isCurrentPage = Some(true)).withChildren( - BreadcrumbLink(text = "breadcrumb-link2").onClick: event => + BreadcrumbLink("breadcrumb-link2", text = "breadcrumb-link2").onClick: event => import event.* handled.withModel(breadcrumbClicked(model, "breadcrumb-link2")) ) From dc0784d6e89f4f946ae62f9058ecc24d5e971b31 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 28 Feb 2024 18:19:00 +0000 Subject: [PATCH 214/313] - --- .../test/scala/org/terminal21/client/ConnectedSessionTest.scala | 2 +- .../org/terminal21/client/json/UiElementEncodingTest.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala index a2f8d3a6..bbe3e893 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala @@ -14,7 +14,7 @@ class ConnectedSessionTest extends AnyFunSuiteLike: test("event iterator"): given connectedSession: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock - val editable = Editable() + val editable = Editable(key = "ed") val it = connectedSession.eventIterator val event1 = OnChange(editable.key, "v1") val event2 = OnChange(editable.key, "v2") diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/json/UiElementEncodingTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/json/UiElementEncodingTest.scala index a79cdccc..594c22ec 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/json/UiElementEncodingTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/json/UiElementEncodingTest.scala @@ -7,6 +7,6 @@ import org.scalatest.matchers.should.Matchers.* class UiElementEncodingTest extends AnyFunSuiteLike: val encoding = new UiElementEncoding(Seq(StdElementEncoding)) test("dataStore"): - val b = Button() + val b = Button(key = "b") val j = encoding.uiElementEncoder(b).deepDropNullValues j.hcursor.downField("Button").downField("dataStore").failed should be(true) From 24e87dd590456810a10fdb6b681d74300f6cc039 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 29 Feb 2024 11:11:34 +0000 Subject: [PATCH 215/313] - --- .../org/terminal21/client/components/StdUiCalculation.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala index 1142af0c..6557b686 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala @@ -15,9 +15,10 @@ //trait StdUiCalculation[OUT]( // name: String, // dataUi: UiElement with HasStyle -//)(using session: ConnectedSession, model: Model[_], executor: FiberExecutor) +//)(using session: ConnectedSession, executor: FiberExecutor) // extends Calculation[OUT] // with UiComponent: +// import Model.Standard.unitModel // private val running = new AtomicBoolean(false) // private val currentUi = new AtomicReference(dataUi) // From e4c96f992eda79bd230f1a1949f7da66633737a6 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 29 Feb 2024 12:10:51 +0000 Subject: [PATCH 216/313] - --- .../client/components/mathjax/MathJax.scala | 5 +- .../client/components/nivo/NivoElement.scala | 9 +- .../org/terminal21/client/Controller.scala | 2 + .../client/components/EventHandler.scala | 7 +- .../client/components/UiElement.scala | 14 +- .../components/chakra/ChakraElement.scala | 440 +++++++++++++----- .../components/chakra/QuickFormControl.scala | 5 +- .../client/components/chakra/QuickTable.scala | 5 +- .../client/components/chakra/QuickTabs.scala | 5 +- .../components/frontend/FrontEndElement.scala | 4 +- .../client/components/std/StdElement.scala | 31 +- .../client/components/std/StdHttp.scala | 4 +- 12 files changed, 393 insertions(+), 138 deletions(-) diff --git a/terminal21-mathjax/src/main/scala/org/terminal21/client/components/mathjax/MathJax.scala b/terminal21-mathjax/src/main/scala/org/terminal21/client/components/mathjax/MathJax.scala index 88d12da5..a44e0622 100644 --- a/terminal21-mathjax/src/main/scala/org/terminal21/client/components/mathjax/MathJax.scala +++ b/terminal21-mathjax/src/main/scala/org/terminal21/client/components/mathjax/MathJax.scala @@ -2,6 +2,7 @@ package org.terminal21.client.components.mathjax import org.terminal21.client.components.UiElement.HasStyle import org.terminal21.client.components.{Keys, UiElement} +import org.terminal21.collections.TypedMap sealed trait MathJaxElement extends UiElement @@ -11,10 +12,12 @@ case class MathJax( key: String = Keys.nextKey, // expression should be like """ text \( asciimath \) text""", i.e. """When \(a \ne 0\), there are two solutions to \(ax^2 + bx + c = 0\)""" expression: String = """fill in the expression as per https://asciimath.org/""", - style: Map[String, Any] = Map.empty // Note: some of the styles are ignored by mathjax lib + style: Map[String, Any] = Map.empty, // Note: some of the styles are ignored by mathjax lib + dataStore: TypedMap = TypedMap.empty ) extends MathJaxElement with HasStyle: type This = MathJax override def withStyle(v: Map[String, Any]): MathJax = copy(style = v) def withKey(k: String) = copy(key = k) def withExpression(e: String) = copy(expression = e) + override def withDataStore(ds: TypedMap): MathJax = copy(dataStore = ds) diff --git a/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/NivoElement.scala b/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/NivoElement.scala index ed91a09d..fc2d5a26 100644 --- a/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/NivoElement.scala +++ b/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/NivoElement.scala @@ -2,6 +2,7 @@ package org.terminal21.client.components.nivo import org.terminal21.client.components.UiElement.HasStyle import org.terminal21.client.components.{Keys, UiElement} +import org.terminal21.collections.TypedMap sealed trait NEJson extends UiElement sealed trait NivoElement extends NEJson with HasStyle @@ -28,12 +29,14 @@ case class ResponsiveLine( pointBorderColor: Map[String, String] = Map("from" -> "serieColor"), pointLabelYOffset: Int = -12, useMesh: Boolean = true, - legends: Seq[Legend] = Nil + legends: Seq[Legend] = Nil, + dataStore: TypedMap = TypedMap.empty ) extends NivoElement: type This = ResponsiveLine override def withStyle(v: Map[String, Any]): ResponsiveLine = copy(style = v) def withKey(v: String) = copy(key = v) def withData(data: Seq[Serie]) = copy(data = data) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** https://nivo.rocks/bar/ */ @@ -57,9 +60,11 @@ case class ResponsiveBar( axisBottom: Option[Axis] = Some(Axis(legend = "y", legendOffset = 36)), axisLeft: Option[Axis] = Some(Axis(legend = "x", legendOffset = -40)), legends: Seq[Legend] = Nil, - ariaLabel: String = "Chart Label" + ariaLabel: String = "Chart Label", + dataStore: TypedMap = TypedMap.empty ) extends NivoElement: type This = ResponsiveBar override def withStyle(v: Map[String, Any]): ResponsiveBar = copy(style = v) def withKey(v: String) = copy(key = v) def withData(data: Seq[Seq[BarDatum]]) = copy(data = data) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 9d9e2a8f..9f977269 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -178,6 +178,8 @@ type OnChangeEventHandlerFunction[M] = ControllerChangeEvent[M] => Handle type OnChangeBooleanEventHandlerFunction[M] = ControllerChangeBooleanEvent[M] => HandledEvent[M] case class Model[M](value: M): + type OnModelChangeFunction = (UiElement, M) => UiElement + object OnModelChangeKey extends TypedMapKey[OnModelChangeFunction] object ClickKey extends TypedMapKey[Seq[OnClickEventHandlerFunction[M]]] object ChangeKey extends TypedMapKey[Seq[OnChangeEventHandlerFunction[M]]] object ChangeBooleanKey extends TypedMapKey[Seq[OnChangeBooleanEventHandlerFunction[M]]] diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala index f9c076ef..cfe020e3 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala @@ -1,12 +1,11 @@ package org.terminal21.client.components -import org.terminal21.client.components.UiElement.HasDataStore import org.terminal21.client.{Model, OnChangeBooleanEventHandlerFunction, OnChangeEventHandlerFunction, OnClickEventHandlerFunction} trait EventHandler object OnClickEventHandler: - trait CanHandleOnClickEvent extends HasDataStore: + trait CanHandleOnClickEvent: this: UiElement => if key.isEmpty then throw new IllegalStateException(s"clickables must have a stable key. Error occurred on $this") def onClick[M](using model: Model[M])(h: OnClickEventHandlerFunction[M]): This = @@ -14,7 +13,7 @@ object OnClickEventHandler: store(model.ClickKey, handlers :+ h) object OnChangeEventHandler: - trait CanHandleOnChangeEvent extends HasDataStore: + trait CanHandleOnChangeEvent: this: UiElement => if key.isEmpty then throw new IllegalStateException(s"changeable must have a stable key. Error occurred on $this") def onChange[M](using model: Model[M])(h: OnChangeEventHandlerFunction[M]): This = @@ -22,7 +21,7 @@ object OnChangeEventHandler: store(model.ChangeKey, handlers :+ h) object OnChangeBooleanEventHandler: - trait CanHandleOnChangeEvent extends HasDataStore: + trait CanHandleOnChangeEvent: this: UiElement => if key.isEmpty then throw new IllegalStateException(s"changeable must have a stable key. Error occurred on $this") def onChange[M](using model: Model[M])(h: OnChangeBooleanEventHandlerFunction[M]): This = diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala index 6e458dd0..27551615 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala @@ -1,5 +1,6 @@ package org.terminal21.client.components +import org.terminal21.client.Model import org.terminal21.client.components.UiElement.HasChildren import org.terminal21.client.components.chakra.Box import org.terminal21.collections.{TypedMap, TypedMapKey} @@ -11,6 +12,13 @@ abstract class UiElement extends AnyElement: def withKey(key: String): This def findKey(key: String): UiElement = flat.find(_.key == key).get + def dataStore: TypedMap + def withDataStore(ds: TypedMap): This + def store[V](key: TypedMapKey[V], value: V): This = withDataStore(dataStore + (key -> value)) + + def onModelChange[M](using model: Model[M])(f: (This, M) => This): This = + store(model.OnModelChangeKey, f.asInstanceOf[model.OnModelChangeFunction]) + /** @return * this element along all it's children flattened */ @@ -38,9 +46,3 @@ object UiElement: def style: Map[String, Any] def withStyle(v: Map[String, Any]): This def withStyle(vs: (String, Any)*): This = withStyle(vs.toMap) - - trait HasDataStore: - this: UiElement => - def dataStore: TypedMap - def withDataStore(ds: TypedMap): This - def store[V](key: TypedMapKey[V], value: V): This = withDataStore(dataStore + (key -> value)) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala index 0f54b002..77a08981 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala @@ -61,7 +61,8 @@ case class ButtonGroup( border: Option[String] = None, borderColor: Option[String] = None, style: Map[String, Any] = Map.empty, - children: Seq[UiElement] = Nil + children: Seq[UiElement] = Nil, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement with HasChildren: type This = ButtonGroup @@ -75,6 +76,7 @@ case class ButtonGroup( def withHeight(v: Option[String]) = copy(height = v) def withBorder(v: Option[String]) = copy(border = v) def withBorderColor(v: Option[String]) = copy(borderColor = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** https://chakra-ui.com/docs/components/box */ @@ -87,7 +89,8 @@ case class Box( color: String = "", style: Map[String, Any] = Map.empty, as: Option[String] = None, - children: Seq[UiElement] = Nil + children: Seq[UiElement] = Nil, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement with HasChildren: type This = Box @@ -100,6 +103,7 @@ case class Box( def withP(v: Int): Box = copy(p = v) def withColor(v: String): Box = copy(color = v) def withAs(v: Option[String]): Box = copy(as = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** https://chakra-ui.com/docs/components/stack */ @@ -108,7 +112,8 @@ case class HStack( spacing: Option[String] = None, align: Option[String] = None, style: Map[String, Any] = Map.empty, - children: Seq[UiElement] = Nil + children: Seq[UiElement] = Nil, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement with HasChildren: type This = HStack @@ -117,13 +122,15 @@ case class HStack( def withKey(v: String) = copy(key = v) def withSpacing(v: Option[String]) = copy(spacing = v) def withAlign(v: Option[String]) = copy(align = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) case class VStack( key: String = Keys.nextKey, spacing: Option[String] = None, align: Option[String] = None, style: Map[String, Any] = Map.empty, - children: Seq[UiElement] = Nil + children: Seq[UiElement] = Nil, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement with HasChildren: type This = VStack @@ -132,6 +139,7 @@ case class VStack( def withKey(v: String) = copy(key = v) def withSpacing(v: Option[String]) = copy(spacing = v) def withAlign(v: Option[String]) = copy(align = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) case class SimpleGrid( key: String = Keys.nextKey, @@ -140,7 +148,8 @@ case class SimpleGrid( spacingY: Option[String] = None, columns: Int = 2, children: Seq[UiElement] = Nil, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement with HasChildren: type This = SimpleGrid @@ -151,6 +160,7 @@ case class SimpleGrid( def withSpacingX(v: Option[String]) = copy(spacingX = v) def withSpacingY(v: Option[String]) = copy(spacingY = v) def withColumns(v: Int) = copy(columns = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** https://chakra-ui.com/docs/components/editable */ @@ -172,20 +182,23 @@ case class Editable( def value = valueReceived.getOrElse(defaultValue) override def withDataStore(ds: TypedMap): Editable = copy(dataStore = ds) -case class EditablePreview(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty) extends ChakraElement: +case class EditablePreview(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) extends ChakraElement: type This = EditablePreview override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) -case class EditableInput(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty) extends ChakraElement: +case class EditableInput(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) extends ChakraElement: type This = EditableInput override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) -case class EditableTextarea(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty) extends ChakraElement: +case class EditableTextarea(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) extends ChakraElement: type This = EditableTextarea override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** https://chakra-ui.com/docs/components/form-control */ @@ -193,7 +206,8 @@ case class FormControl( key: String = Keys.nextKey, as: String = "", style: Map[String, Any] = Map.empty, - children: Seq[UiElement] = Nil + children: Seq[UiElement] = Nil, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement with HasChildren: type This = FormControl @@ -201,6 +215,7 @@ case class FormControl( override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withAs(v: String) = copy(as = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** https://chakra-ui.com/docs/components/form-control */ @@ -208,7 +223,8 @@ case class FormLabel( key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty, - children: Seq[UiElement] = Nil + children: Seq[UiElement] = Nil, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement with HasChildren: type This = FormLabel @@ -216,6 +232,7 @@ case class FormLabel( override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** https://chakra-ui.com/docs/components/form-control */ @@ -223,7 +240,8 @@ case class FormHelperText( key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty, - children: Seq[UiElement] = Nil + children: Seq[UiElement] = Nil, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement with HasChildren: type This = FormHelperText @@ -231,6 +249,7 @@ case class FormHelperText( override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** https://chakra-ui.com/docs/components/input */ @@ -261,7 +280,8 @@ case class InputGroup( key: String = Keys.nextKey, size: String = "md", style: Map[String, Any] = Map.empty, - children: Seq[UiElement] = Nil + children: Seq[UiElement] = Nil, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement with HasChildren: type This = InputGroup @@ -269,12 +289,14 @@ case class InputGroup( override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withSize(v: String) = copy(size = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) case class InputLeftAddon( key: String = Keys.nextKey, text: String = "", style: Map[String, Any] = Map.empty, - children: Seq[UiElement] = Nil + children: Seq[UiElement] = Nil, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement with HasChildren: type This = InputLeftAddon @@ -282,12 +304,14 @@ case class InputLeftAddon( override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) case class InputRightAddon( key: String = Keys.nextKey, text: String = "", style: Map[String, Any] = Map.empty, - children: Seq[UiElement] = Nil + children: Seq[UiElement] = Nil, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement with HasChildren: type This = InputRightAddon @@ -295,6 +319,7 @@ case class InputRightAddon( override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** https://chakra-ui.com/docs/components/checkbox */ @@ -324,7 +349,8 @@ case class Radio( value: String, text: String = "", colorScheme: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = Radio override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -332,6 +358,7 @@ case class Radio( def withValue(v: String) = copy(value = v) def withText(v: String) = copy(text = v) def withColorScheme(v: Option[String]) = copy(colorScheme = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) case class RadioGroup( key: String = Keys.nextKey, @@ -359,7 +386,8 @@ case class Center( w: Option[String] = None, h: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement with HasChildren: type This = Center @@ -371,6 +399,7 @@ case class Center( def withW(v: Option[String]) = copy(w = v) def withH(v: Option[String]) = copy(h = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) case class Circle( key: String = Keys.nextKey, @@ -380,7 +409,8 @@ case class Circle( w: Option[String] = None, h: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement with HasChildren: type This = Circle @@ -392,6 +422,7 @@ case class Circle( def withW(v: Option[String]) = copy(w = v) def withH(v: Option[String]) = copy(h = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) case class Square( key: String = Keys.nextKey, @@ -401,7 +432,8 @@ case class Square( w: Option[String] = None, h: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement with HasChildren: type This = Square @@ -413,6 +445,7 @@ case class Square( def withW(v: Option[String]) = copy(w = v) def withH(v: Option[String]) = copy(h = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -422,7 +455,8 @@ case class AddIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = AddIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -431,6 +465,7 @@ case class AddIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -440,7 +475,8 @@ case class ArrowBackIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = ArrowBackIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -449,6 +485,7 @@ case class ArrowBackIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -458,7 +495,8 @@ case class ArrowDownIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = ArrowDownIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -467,6 +505,7 @@ case class ArrowDownIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -476,7 +515,8 @@ case class ArrowForwardIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = ArrowForwardIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -485,6 +525,7 @@ case class ArrowForwardIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -494,7 +535,8 @@ case class ArrowLeftIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = ArrowLeftIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -503,6 +545,7 @@ case class ArrowLeftIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -512,7 +555,8 @@ case class ArrowRightIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = ArrowRightIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -521,6 +565,7 @@ case class ArrowRightIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -530,7 +575,8 @@ case class ArrowUpIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = ArrowUpIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -539,6 +585,7 @@ case class ArrowUpIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -548,7 +595,8 @@ case class ArrowUpDownIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = ArrowUpDownIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -557,6 +605,7 @@ case class ArrowUpDownIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -566,7 +615,8 @@ case class AtSignIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = AtSignIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -575,6 +625,7 @@ case class AtSignIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -584,7 +635,8 @@ case class AttachmentIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = AttachmentIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -593,6 +645,7 @@ case class AttachmentIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -602,7 +655,8 @@ case class BellIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = BellIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -611,6 +665,7 @@ case class BellIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -620,7 +675,8 @@ case class CalendarIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = CalendarIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -629,6 +685,7 @@ case class CalendarIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -638,7 +695,8 @@ case class ChatIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = ChatIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -647,6 +705,7 @@ case class ChatIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -656,7 +715,8 @@ case class CheckIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = CheckIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -665,6 +725,7 @@ case class CheckIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -674,7 +735,8 @@ case class CheckCircleIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = CheckCircleIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -683,6 +745,7 @@ case class CheckCircleIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -692,7 +755,8 @@ case class ChevronDownIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = ChevronDownIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -701,6 +765,7 @@ case class ChevronDownIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -710,7 +775,8 @@ case class ChevronLeftIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = ChevronLeftIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -719,6 +785,7 @@ case class ChevronLeftIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -728,7 +795,8 @@ case class ChevronRightIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = ChevronRightIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -737,6 +805,7 @@ case class ChevronRightIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -746,7 +815,8 @@ case class ChevronUpIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = ChevronUpIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -755,6 +825,7 @@ case class ChevronUpIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -764,7 +835,8 @@ case class CloseIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = CloseIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -773,6 +845,7 @@ case class CloseIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -782,7 +855,8 @@ case class CopyIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = CopyIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -791,6 +865,7 @@ case class CopyIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -800,7 +875,8 @@ case class DeleteIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = DeleteIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -809,6 +885,7 @@ case class DeleteIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -818,7 +895,8 @@ case class DownloadIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = DownloadIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -827,6 +905,7 @@ case class DownloadIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -836,7 +915,8 @@ case class DragHandleIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = DragHandleIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -845,6 +925,7 @@ case class DragHandleIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -854,7 +935,8 @@ case class EditIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = EditIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -863,6 +945,7 @@ case class EditIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -872,7 +955,8 @@ case class EmailIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = EmailIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -881,6 +965,7 @@ case class EmailIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -891,7 +976,8 @@ case class ExternalLinkIcon( mx: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = ExternalLinkIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -901,6 +987,7 @@ case class ExternalLinkIcon( def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) def withMx(v: Option[String]) = copy(mx = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -910,7 +997,8 @@ case class HamburgerIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = HamburgerIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -919,6 +1007,7 @@ case class HamburgerIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -928,7 +1017,8 @@ case class InfoIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = InfoIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -937,6 +1027,7 @@ case class InfoIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -946,7 +1037,8 @@ case class InfoOutlineIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = InfoOutlineIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -955,6 +1047,7 @@ case class InfoOutlineIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -964,7 +1057,8 @@ case class LinkIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = LinkIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -973,6 +1067,7 @@ case class LinkIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -982,7 +1077,8 @@ case class LockIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = LockIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -991,6 +1087,7 @@ case class LockIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -1000,7 +1097,8 @@ case class MinusIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = MinusIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1009,6 +1107,7 @@ case class MinusIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -1018,7 +1117,8 @@ case class MoonIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = MoonIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1027,6 +1127,7 @@ case class MoonIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -1036,7 +1137,8 @@ case class NotAllowedIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = NotAllowedIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1045,6 +1147,7 @@ case class NotAllowedIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -1054,7 +1157,8 @@ case class PhoneIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = PhoneIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1063,6 +1167,7 @@ case class PhoneIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -1072,7 +1177,8 @@ case class PlusSquareIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = PlusSquareIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1081,6 +1187,7 @@ case class PlusSquareIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -1090,7 +1197,8 @@ case class QuestionIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = QuestionIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1099,6 +1207,7 @@ case class QuestionIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -1108,7 +1217,8 @@ case class QuestionOutlineIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = QuestionOutlineIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1117,6 +1227,7 @@ case class QuestionOutlineIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -1126,7 +1237,8 @@ case class RepeatIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = RepeatIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1135,6 +1247,7 @@ case class RepeatIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -1144,7 +1257,8 @@ case class RepeatClockIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = RepeatClockIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1153,6 +1267,7 @@ case class RepeatClockIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -1162,7 +1277,8 @@ case class SearchIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = SearchIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1171,6 +1287,7 @@ case class SearchIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -1180,7 +1297,8 @@ case class Search2Icon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = Search2Icon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1189,6 +1307,7 @@ case class Search2Icon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -1198,7 +1317,8 @@ case class SettingsIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = SettingsIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1207,6 +1327,7 @@ case class SettingsIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -1216,7 +1337,8 @@ case class SmallAddIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = SmallAddIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1225,6 +1347,7 @@ case class SmallAddIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -1234,7 +1357,8 @@ case class SmallCloseIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = SmallCloseIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1243,6 +1367,7 @@ case class SmallCloseIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -1252,7 +1377,8 @@ case class SpinnerIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = SpinnerIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1261,6 +1387,7 @@ case class SpinnerIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -1270,7 +1397,8 @@ case class StarIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = StarIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1279,6 +1407,7 @@ case class StarIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -1288,7 +1417,8 @@ case class SunIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = SunIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1297,6 +1427,7 @@ case class SunIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -1306,7 +1437,8 @@ case class TimeIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = TimeIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1315,6 +1447,7 @@ case class TimeIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -1324,7 +1457,8 @@ case class TriangleDownIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = TriangleDownIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1333,6 +1467,7 @@ case class TriangleDownIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -1342,7 +1477,8 @@ case class TriangleUpIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = TriangleUpIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1351,6 +1487,7 @@ case class TriangleUpIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -1360,7 +1497,8 @@ case class UnlockIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = UnlockIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1369,6 +1507,7 @@ case class UnlockIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -1378,7 +1517,8 @@ case class UpDownIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = UpDownIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1387,6 +1527,7 @@ case class UpDownIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -1396,7 +1537,8 @@ case class ViewIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = ViewIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1405,6 +1547,7 @@ case class ViewIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -1414,7 +1557,8 @@ case class ViewOffIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = ViewOffIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1423,6 +1567,7 @@ case class ViewOffIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -1432,7 +1577,8 @@ case class WarningIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = WarningIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1441,6 +1587,7 @@ case class WarningIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ @@ -1450,7 +1597,8 @@ case class WarningTwoIcon( h: Option[String] = None, boxSize: Option[String] = None, color: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = WarningTwoIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1459,6 +1607,7 @@ case class WarningTwoIcon( def withH(v: Option[String]) = copy(h = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withColor(v: Option[String]) = copy(color = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** https://chakra-ui.com/docs/components/textarea */ @@ -1538,17 +1687,19 @@ case class Option_( key: String = Keys.nextKey, value: String, text: String = "", - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = Option_ override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withValue(v: String) = copy(value = v) def withText(v: String) = copy(text = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** https://chakra-ui.com/docs/components/table/usage */ -case class TableContainer(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty) +case class TableContainer(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) extends ChakraElement with HasChildren: type This = TableContainer @@ -1571,6 +1722,7 @@ case class TableContainer(key: String = Keys.nextKey, children: Seq[UiElement] = this override def withChildren(cn: UiElement*) = copy(children = cn) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) case class Table( key: String = Keys.nextKey, @@ -1578,7 +1730,8 @@ case class Table( size: String = "md", colorScheme: Option[String] = None, style: Map[String, Any] = Map.empty, - children: Seq[UiElement] = Nil + children: Seq[UiElement] = Nil, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement with HasChildren: type This = Table @@ -1588,48 +1741,63 @@ case class Table( def withVariant(v: String) = copy(variant = v) def withSize(v: String) = copy(size = v) def withColorScheme(v: Option[String]) = copy(colorScheme = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) -case class TableCaption(key: String = Keys.nextKey, text: String = "", style: Map[String, Any] = Map.empty) extends ChakraElement: +case class TableCaption(key: String = Keys.nextKey, text: String = "", style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) + extends ChakraElement: type This = TableCaption override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) -case class Thead(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty) extends ChakraElement with HasChildren: +case class Thead(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) + extends ChakraElement + with HasChildren: type This = Thead override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) -case class Tbody(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty) extends ChakraElement with HasChildren: +case class Tbody(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) + extends ChakraElement + with HasChildren: type This = Tbody override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) -case class Tfoot(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty) extends ChakraElement with HasChildren: +case class Tfoot(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) + extends ChakraElement + with HasChildren: type This = Tfoot override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) case class Tr( key: String = Keys.nextKey, children: Seq[UiElement] = Nil, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement with HasChildren: type This = Tr override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) case class Th( key: String = Keys.nextKey, text: String = "", isNumeric: Boolean = false, children: Seq[UiElement] = Nil, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement with HasChildren: type This = Th @@ -1638,13 +1806,15 @@ case class Th( def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) def withIsNumeric(v: Boolean) = copy(isNumeric = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) case class Td( key: String = Keys.nextKey, text: String = "", isNumeric: Boolean = false, style: Map[String, Any] = Map.empty, - children: Seq[UiElement] = Nil + children: Seq[UiElement] = Nil, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement with HasChildren: type This = Td @@ -1653,14 +1823,18 @@ case class Td( def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) def withIsNumeric(v: Boolean) = copy(isNumeric = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** https://chakra-ui.com/docs/components/menu/usage */ -case class Menu(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil) extends ChakraElement with HasChildren: +case class Menu(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, dataStore: TypedMap = TypedMap.empty) + extends ChakraElement + with HasChildren: type This = Menu override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) case class MenuButton( key: String = Keys.nextKey, @@ -1668,7 +1842,8 @@ case class MenuButton( size: Option[String] = None, colorScheme: Option[String] = None, style: Map[String, Any] = Map.empty, - children: Seq[UiElement] = Nil + children: Seq[UiElement] = Nil, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement with HasChildren: type This = MenuButton @@ -1678,12 +1853,16 @@ case class MenuButton( def withText(v: String) = copy(text = v) def withSize(v: Option[String]) = copy(size = v) def withColorScheme(v: Option[String]) = copy(colorScheme = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) -case class MenuList(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil) extends ChakraElement with HasChildren: +case class MenuList(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, dataStore: TypedMap = TypedMap.empty) + extends ChakraElement + with HasChildren: type This = MenuList override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) case class MenuItem( key: String = Keys.nextKey, @@ -1701,10 +1880,11 @@ case class MenuItem( def withText(v: String) = copy(text = v) override def withDataStore(ds: TypedMap): MenuItem = copy(dataStore = ds) -case class MenuDivider(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty) extends ChakraElement: +case class MenuDivider(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) extends ChakraElement: type This = MenuDivider override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) case class Badge( key: String = Keys.nextKey, @@ -1713,7 +1893,8 @@ case class Badge( variant: Option[String] = None, size: String = "md", children: Seq[UiElement] = Nil, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement with HasChildren: type This = Badge @@ -1724,6 +1905,7 @@ case class Badge( def withColorScheme(v: Option[String]) = copy(colorScheme = v) def withVariant(v: Option[String]) = copy(variant = v) def withSize(v: String) = copy(size = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** https://chakra-ui.com/docs/components/image/usage * @@ -1737,7 +1919,8 @@ case class Image( alt: String = "", boxSize: Option[String] = None, borderRadius: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = Image override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1746,6 +1929,7 @@ case class Image( def withAlt(v: String) = copy(alt = v) def withBoxSize(v: Option[String]) = copy(boxSize = v) def withBorderRadius(v: Option[String]) = copy(borderRadius = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** https://chakra-ui.com/docs/components/text */ @@ -1759,7 +1943,8 @@ case class Text( align: Option[String] = None, casing: Option[String] = None, decoration: Option[String] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = Text override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1772,13 +1957,15 @@ case class Text( def withAlign(v: Option[String]) = copy(align = v) def withCasing(v: Option[String]) = copy(casing = v) def withDecoration(v: Option[String]) = copy(decoration = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) case class Code( key: String = Keys.nextKey, text: String = "", colorScheme: Option[String] = None, style: Map[String, Any] = Map.empty, - children: Seq[UiElement] = Nil + children: Seq[UiElement] = Nil, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement with HasChildren: type This = Code @@ -1787,12 +1974,14 @@ case class Code( def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) def withColorScheme(v: Option[String]) = copy(colorScheme = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) case class UnorderedList( key: String = Keys.nextKey, spacing: Option[String] = None, style: Map[String, Any] = Map.empty, - children: Seq[UiElement] = Nil + children: Seq[UiElement] = Nil, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement with HasChildren: type This = UnorderedList @@ -1800,12 +1989,14 @@ case class UnorderedList( override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withSpacing(v: Option[String]) = copy(spacing = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) case class OrderedList( key: String = Keys.nextKey, spacing: Option[String] = None, style: Map[String, Any] = Map.empty, - children: Seq[UiElement] = Nil + children: Seq[UiElement] = Nil, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement with HasChildren: type This = OrderedList @@ -1813,12 +2004,14 @@ case class OrderedList( override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withSpacing(v: Option[String]) = copy(spacing = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) case class ListItem( key: String = Keys.nextKey, text: String = "", style: Map[String, Any] = Map.empty, - children: Seq[UiElement] = Nil + children: Seq[UiElement] = Nil, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement with HasChildren: type This = ListItem @@ -1826,12 +2019,14 @@ case class ListItem( override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) case class Alert( key: String = Keys.nextKey, status: String = "error", // error, success, warning, info style: Map[String, Any] = Map.empty, - children: Seq[UiElement] = Nil + children: Seq[UiElement] = Nil, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement with HasChildren: type This = Alert @@ -1839,34 +2034,41 @@ case class Alert( override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withStatus(v: String) = copy(status = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) case class AlertIcon( key: String = Keys.nextKey, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = AlertIcon override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) case class AlertTitle( key: String = Keys.nextKey, text: String = "Alert!", - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = AlertTitle override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) case class AlertDescription( key: String = Keys.nextKey, text: String = "Something happened!", - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = AlertDescription override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** https://chakra-ui.com/docs/components/progress */ @@ -1877,7 +2079,8 @@ case class Progress( size: Option[String] = None, hasStripe: Option[Boolean] = None, isIndeterminate: Option[Boolean] = None, - style: Map[String, Any] = Map.empty + style: Map[String, Any] = Map.empty, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement: type This = Progress override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1887,6 +2090,7 @@ case class Progress( def withValue(v: Int) = copy(value = v) def withHasStripe(v: Option[Boolean]) = copy(hasStripe = v) def withIsIndeterminate(v: Option[Boolean]) = copy(isIndeterminate = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) case class Tooltip( key: String = Keys.nextKey, @@ -1896,7 +2100,8 @@ case class Tooltip( hasArrow: Option[Boolean] = None, fontSize: Option[String] = None, style: Map[String, Any] = Map.empty, - children: Seq[UiElement] = Seq(Text("use tooltip.withContent() to set this")) + children: Seq[UiElement] = Seq(Text("use tooltip.withContent() to set this")), + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement with HasChildren: type This = Tooltip @@ -1910,6 +2115,7 @@ case class Tooltip( override def noChildren = copy(children = Nil) override def withChildren(cn: UiElement*): Tooltip = if cn.size != 1 then throw new IllegalArgumentException("tooltip takes 1 only child") else copy(children = cn) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** see https://chakra-ui.com/docs/components/tabs */ @@ -1922,7 +2128,8 @@ case class Tabs( size: Option[String] = None, isFitted: Option[Boolean] = None, style: Map[String, Any] = Map.empty, - children: Seq[UiElement] = Nil + children: Seq[UiElement] = Nil, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement with HasChildren: type This = Tabs @@ -1934,19 +2141,22 @@ case class Tabs( def withSize(v: Option[String]) = copy(size = v) def withAlign(v: Option[String]) = copy(align = v) def withIsFitted(v: Option[Boolean]) = copy(isFitted = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** see https://chakra-ui.com/docs/components/tabs */ case class TabList( key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, - children: Seq[UiElement] = Nil + children: Seq[UiElement] = Nil, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement with HasChildren: type This = TabList def withKey(v: String) = copy(key = v) override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** see https://chakra-ui.com/docs/components/tabs */ @@ -1958,7 +2168,8 @@ case class Tab( _hover: Option[Map[String, Any]] = None, _active: Option[Map[String, Any]] = None, style: Map[String, Any] = Map.empty, - children: Seq[UiElement] = Nil + children: Seq[UiElement] = Nil, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement with HasChildren: type This = Tab @@ -1973,32 +2184,37 @@ case class Tab( def withHover(v: Option[Map[String, Any]]) = copy(_hover = v) def withActive(v: Map[String, Any]) = copy(_active = Some(v)) def withActive(v: Option[Map[String, Any]]) = copy(_active = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** see https://chakra-ui.com/docs/components/tabs */ case class TabPanels( key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, - children: Seq[UiElement] = Nil + children: Seq[UiElement] = Nil, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement with HasChildren: type This = TabPanels def withKey(v: String) = copy(key = v) override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** see https://chakra-ui.com/docs/components/tabs */ case class TabPanel( key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, - children: Seq[UiElement] = Nil + children: Seq[UiElement] = Nil, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement with HasChildren: type This = TabPanel def withKey(v: String) = copy(key = v) override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** https://chakra-ui.com/docs/components/breadcrumb */ @@ -2010,7 +2226,8 @@ case class Breadcrumb( fontSize: Option[String] = None, pt: Option[Int] = None, style: Map[String, Any] = Map.empty, - children: Seq[UiElement] = Nil + children: Seq[UiElement] = Nil, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement with HasChildren: type This = Breadcrumb @@ -2022,6 +2239,7 @@ case class Breadcrumb( def withFontWeight(v: Option[String]) = copy(fontWeight = v) def withFontSize(v: Option[String]) = copy(fontSize = v) def withPt(v: Option[Int]) = copy(pt = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** https://chakra-ui.com/docs/components/breadcrumb */ @@ -2029,7 +2247,8 @@ case class BreadcrumbItem( key: String = Keys.nextKey, isCurrentPage: Option[Boolean] = None, style: Map[String, Any] = Map.empty, - children: Seq[UiElement] = Nil + children: Seq[UiElement] = Nil, + dataStore: TypedMap = TypedMap.empty ) extends ChakraElement with HasChildren: type This = BreadcrumbItem @@ -2037,6 +2256,7 @@ case class BreadcrumbItem( override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withIsCurrentPage(v: Option[Boolean]) = copy(isCurrentPage = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** https://chakra-ui.com/docs/components/breadcrumb */ diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala index e31a6b86..f5bb399c 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala @@ -2,13 +2,15 @@ package org.terminal21.client.components.chakra import org.terminal21.client.components.UiElement.HasStyle import org.terminal21.client.components.{Keys, UiComponent, UiElement} +import org.terminal21.collections.TypedMap case class QuickFormControl( key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, label: Option[String] = None, inputGroup: Seq[UiElement] = Nil, - helperText: Option[String] = None + helperText: Option[String] = None, + dataStore: TypedMap = TypedMap.empty ) extends UiComponent with HasStyle: type This = QuickFormControl @@ -25,3 +27,4 @@ case class QuickFormControl( override def withStyle(v: Map[String, Any]): QuickFormControl = copy(style = v) override def withKey(key: String): QuickFormControl = copy(key = key) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala index 8c60425a..9f1c883e 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala @@ -2,6 +2,7 @@ package org.terminal21.client.components.chakra import org.terminal21.client.components.UiElement.HasStyle import org.terminal21.client.components.{Keys, UiComponent, UiElement} +import org.terminal21.collections.TypedMap case class QuickTable( key: String = Keys.nextKey, @@ -11,7 +12,8 @@ case class QuickTable( style: Map[String, Any] = Map.empty, caption: Option[String] = None, headers: Seq[UiElement] = Nil, - rows: Seq[Seq[UiElement]] = Nil + rows: Seq[Seq[UiElement]] = Nil, + dataStore: TypedMap = TypedMap.empty ) extends UiComponent with HasStyle: type This = QuickTable @@ -56,3 +58,4 @@ case class QuickTable( def caption(text: String): QuickTable = copy(caption = Some(text)) override def withStyle(v: Map[String, Any]): QuickTable = copy(style = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala index 645a310f..5b5cdda6 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala @@ -2,12 +2,14 @@ package org.terminal21.client.components.chakra import org.terminal21.client.components.UiElement.HasStyle import org.terminal21.client.components.{Keys, UiComponent, UiElement} +import org.terminal21.collections.TypedMap case class QuickTabs( key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, tabs: Seq[String | Seq[UiElement]] = Nil, - tabPanels: Seq[Seq[UiElement]] = Nil + tabPanels: Seq[Seq[UiElement]] = Nil, + dataStore: TypedMap = TypedMap.empty ) extends UiComponent with HasStyle: type This = QuickTabs @@ -34,3 +36,4 @@ case class QuickTabs( override def withStyle(v: Map[String, Any]): QuickTabs = copy(style = v) override def withKey(key: String): QuickTabs = copy(key = key) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/frontend/FrontEndElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/frontend/FrontEndElement.scala index b48b82f2..b4ff803b 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/frontend/FrontEndElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/frontend/FrontEndElement.scala @@ -1,9 +1,11 @@ package org.terminal21.client.components.frontend import org.terminal21.client.components.{Keys, UiElement} +import org.terminal21.collections.TypedMap sealed trait FrontEndElement extends UiElement -case class ThemeToggle(key: String = Keys.nextKey) extends FrontEndElement: +case class ThemeToggle(key: String = Keys.nextKey, dataStore: TypedMap = TypedMap.empty) extends FrontEndElement: override type This = ThemeToggle override def withKey(key: String): ThemeToggle = copy(key = key) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala index d4e2e66f..3516c03e 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala @@ -8,64 +8,74 @@ import org.terminal21.collections.TypedMap sealed trait StdEJson extends UiElement sealed trait StdElement extends StdEJson with HasStyle -case class Span(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement: +case class Span(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) extends StdElement: type This = Span override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) -case class NewLine(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty) extends StdElement: +case class NewLine(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) extends StdElement: type This = NewLine override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) -case class Em(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement: +case class Em(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) extends StdElement: type This = Em override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) -case class Header1(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement: +case class Header1(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) extends StdElement: type This = Header1 override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) -case class Header2(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement: +case class Header2(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) extends StdElement: type This = Header2 override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) -case class Header3(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement: +case class Header3(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) extends StdElement: type This = Header3 override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) -case class Header4(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement: +case class Header4(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) extends StdElement: type This = Header4 override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) -case class Header5(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement: +case class Header5(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) extends StdElement: type This = Header5 override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) -case class Header6(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement: +case class Header6(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) extends StdElement: type This = Header6 override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) case class Paragraph( key: String = Keys.nextKey, text: String = "", style: Map[String, Any] = Map.empty, - children: Seq[UiElement] = Nil + children: Seq[UiElement] = Nil, + dataStore: TypedMap = TypedMap.empty ) extends StdElement with HasChildren: type This = Paragraph @@ -73,6 +83,7 @@ case class Paragraph( override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) case class Input( key: String = Keys.nextKey, diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala index e952e405..2bd7c33a 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala @@ -29,10 +29,12 @@ case class Cookie( value: String = "cookie.value", path: Option[String] = None, expireDays: Option[Int] = None, - requestId: String = TransientRequest.newRequestId() + requestId: String = TransientRequest.newRequestId(), + dataStore: TypedMap = TypedMap.empty ) extends StdHttp: override type This = Cookie override def withKey(key: String): Cookie = copy(key = key) + override def withDataStore(ds: TypedMap) = copy(dataStore = ds) /** Read a cookie value. The value, when read from the ui, it will reflect in `value` assuming the UI had the time to send the value back. Also the onChange * handler will be called once with the value. From 50184f140a7d5e3ce5c662956b974192b9a7350b Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 29 Feb 2024 13:00:35 +0000 Subject: [PATCH 217/313] - --- .../org/terminal21/collections/TypedMap.scala | 9 +- .../terminal21/collections/TypedMapTest.scala | 31 ++++++- .../org/terminal21/client/Controller.scala | 46 ++++++---- .../terminal21/client/components/Keys.scala | 17 +--- .../terminal21/client/ControllerTest.scala | 91 +++++++++++-------- .../client/components/KeysTest.scala | 22 ----- 6 files changed, 124 insertions(+), 92 deletions(-) delete mode 100644 terminal21-ui-std/src/test/scala/org/terminal21/client/components/KeysTest.scala diff --git a/terminal21-server-client-common/src/main/scala/org/terminal21/collections/TypedMap.scala b/terminal21-server-client-common/src/main/scala/org/terminal21/collections/TypedMap.scala index 48eb3cc0..6ede3e03 100644 --- a/terminal21-server-client-common/src/main/scala/org/terminal21/collections/TypedMap.scala +++ b/terminal21-server-client-common/src/main/scala/org/terminal21/collections/TypedMap.scala @@ -3,8 +3,15 @@ package org.terminal21.collections class TypedMap(val m: Map[TypedMapKey[_], Any]): def +[A](kv: (TypedMapKey[A], A)): TypedMap = new TypedMap(m + kv) def apply[A](k: TypedMapKey[A]): A = m(k).asInstanceOf[A] + def get[A](k: TypedMapKey[A]): Option[A] = m.get(k).asInstanceOf[Option[A]] def getOrElse[A](k: TypedMapKey[A], default: => A) = m.getOrElse(k, default).asInstanceOf[A] - def contains[A](k: TypedMapKey[A]) = m.contains(k) + + override def hashCode() = m.hashCode() + override def equals(obj: Any) = obj match + case tm: TypedMap => m == tm.m + case _ => false + + def contains[A](k: TypedMapKey[A]) = m.contains(k) object TypedMap: def empty = new TypedMap(Map.empty) diff --git a/terminal21-server-client-common/src/test/scala/org/terminal21/collections/TypedMapTest.scala b/terminal21-server-client-common/src/test/scala/org/terminal21/collections/TypedMapTest.scala index ef5e8547..674592b3 100644 --- a/terminal21-server-client-common/src/test/scala/org/terminal21/collections/TypedMapTest.scala +++ b/terminal21-server-client-common/src/test/scala/org/terminal21/collections/TypedMapTest.scala @@ -2,17 +2,21 @@ package org.terminal21.collections import org.scalatest.funsuite.AnyFunSuiteLike import org.scalatest.matchers.should.Matchers.* -import org.terminal21.collections.{TypedMap, TypedMapKey} class TypedMapTest extends AnyFunSuiteLike: object IntKey extends TypedMapKey[Int] object StringKey extends TypedMapKey[String] - test("add and get"): + test("apply"): val m = TypedMap.empty + (IntKey -> 5) + (StringKey -> "x") m(IntKey) should be(5) m(StringKey) should be("x") + test("get"): + val m = TypedMap.empty + (IntKey -> 5) + (StringKey -> "x") + m.get(IntKey) should be(Some(5)) + m.get(StringKey) should be(Some("x")) + test("getOrElse when key not available"): TypedMap.empty.getOrElse(IntKey, 2) should be(2) @@ -24,3 +28,26 @@ class TypedMapTest extends AnyFunSuiteLike: test("contains key negative"): TypedMap.empty.contains(IntKey) should be(false) + + test("get key negative"): + TypedMap.empty.get(IntKey) should be(None) + + test("equals positive"): + val m1 = TypedMap.empty + (IntKey -> 5) + val m2 = TypedMap.empty + (IntKey -> 5) + m1 should be(m2) + + test("equals negative"): + val m1 = TypedMap.empty + (IntKey -> 5) + val m2 = TypedMap.empty + (IntKey -> 6) + m1 should not be m2 + + test("hashCode positive"): + val m1 = TypedMap.empty + (IntKey -> 5) + val m2 = TypedMap.empty + (IntKey -> 5) + m1.hashCode should be(m2.hashCode) + + test("hashCode negative"): + val m1 = TypedMap.empty + (IntKey -> 5) + val m2 = TypedMap.empty + (IntKey -> 6) + m1.hashCode should not be m2.hashCode diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 9f977269..3246cd21 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -5,23 +5,21 @@ import org.terminal21.client.collections.EventIterator import org.terminal21.client.components.OnChangeEventHandler.CanHandleOnChangeEvent import org.terminal21.client.components.OnClickEventHandler.CanHandleOnClickEvent import org.terminal21.client.components.{Keys, OnChangeBooleanEventHandler, OnChangeEventHandler, OnClickEventHandler, UiElement} -import org.terminal21.collections.TypedMapKey +import org.terminal21.collections.{TypedMap, TypedMapKey} import org.terminal21.model.{ClientEvent, CommandEvent, OnChange, OnClick} class Controller[M]( eventIteratorFactory: => Iterator[CommandEvent], renderChanges: Seq[UiElement] => Unit, - modelComponents: M => Seq[UiElement], + modelComponents: Seq[UiElement], initialModel: Model[M], eventHandlers: Seq[PartialFunction[ControllerEvent[M], HandledEvent[M]]] ): - private def prepareComponents(m: M): Seq[UiElement] = - Keys.linearKeys(modelComponents(m).map(_.substituteComponents)) def render()(using session: ConnectedSession): RenderedController[M] = - val initComponents = prepareComponents(initialModel.value) + val initComponents = modelComponents.map(_.substituteComponents) session.render(initComponents) - new RenderedController(eventIteratorFactory, initialModel, initComponents, prepareComponents, renderChanges, eventHandlers) + new RenderedController(eventIteratorFactory, initialModel, initComponents, renderChanges, eventHandlers) def onEvent(handler: PartialFunction[ControllerEvent[M], HandledEvent[M]]) = new Controller( @@ -36,7 +34,6 @@ class RenderedController[M]( eventIteratorFactory: => Iterator[CommandEvent], initialModel: Model[M], initialComponents: Seq[UiElement], - modelComponents: M => Seq[UiElement], renderChanges: Seq[UiElement] => Unit, eventHandlers: Seq[PartialFunction[ControllerEvent[M], HandledEvent[M]]] ): @@ -119,17 +116,31 @@ class RenderedController[M]( private def doRenderChanges(oldHandled: HandledEvent[M], newHandled: HandledEvent[M]): HandledEvent[M] = // TODO: optimise what elements are rendered - val all = modelComponents(newHandled.model) - renderChanges(all) - newHandled.copy(componentsByKey = calcComponentsByKeyMap(all)) + val changeFunctions = + for + e <- newHandled.componentsByKey.values + f <- e.dataStore.get(initialModel.OnModelChangeKey) + yield (e, f) + + val dsEmpty = TypedMap.empty + val changed = changeFunctions + .map: (e, f) => + (e, f(e, newHandled.model)) + .filter: (e, ne) => + e.withDataStore(dsEmpty) != ne.withDataStore(dsEmpty) + .map(_._2) + .toList + renderChanges(changed) + newHandled.copy(componentsByKey = calcComponentsByKeyMap(changed), renderedChanges = changed) def handledEventsIterator: EventIterator[HandledEvent[M]] = - val initHandled = HandledEvent(initialModel.value, calcComponentsByKeyMap(initialComponents), false) + val initHandled = HandledEvent(initialModel.value, calcComponentsByKeyMap(initialComponents), false, Nil) new EventIterator( eventIteratorFactory .takeWhile(!_.isSessionClosed) .scanLeft((initHandled, initHandled)): case ((_, oldHandled), event) => + println(event) try val handled2 = invokeEventHandlers(oldHandled, event) val handled3 = invokeComponentEventHandlers(handled2, event) @@ -139,7 +150,7 @@ class RenderedController[M]( logger.error("an error occurred while iterating events", t) (oldHandled, oldHandled) .map: (oldHandled, newHandled) => - if oldHandled.model != newHandled.model then doRenderChanges(oldHandled, newHandled) else newHandled + if oldHandled.model != newHandled.model then doRenderChanges(oldHandled, newHandled) else newHandled.copy(renderedChanges = Nil) .flatMap: h => // trick to make sure we take the last state of the model when shouldTerminate=true if h.shouldTerminate then Seq(h.copy(shouldTerminate = false), h) else Seq(h) @@ -147,12 +158,12 @@ class RenderedController[M]( ) object Controller: - def apply[M](initialModel: Model[M], modelComponents: M => Seq[UiElement])(using session: ConnectedSession): Controller[M] = + def apply[M](initialModel: Model[M], modelComponents: Seq[UiElement])(using session: ConnectedSession): Controller[M] = new Controller(session.eventIterator, session.renderChanges, modelComponents, initialModel, Nil) - def apply[M](modelComponents: M => Seq[UiElement])(using initialModel: Model[M], session: ConnectedSession): Controller[M] = + def apply[M](modelComponents: Seq[UiElement])(using initialModel: Model[M], session: ConnectedSession): Controller[M] = new Controller(session.eventIterator, session.renderChanges, modelComponents, initialModel, Nil) - def noModel(modelComponents: Seq[UiElement])(using session: ConnectedSession): Controller[Unit] = - new Controller(session.eventIterator, session.renderChanges, _ => modelComponents, Model.Standard.unitModel, Nil) + def noModel(modelComponents: Seq[UiElement])(using session: ConnectedSession): Controller[Unit] = + new Controller(session.eventIterator, session.renderChanges, modelComponents, Model.Standard.unitModel, Nil) sealed trait ControllerEvent[M]: def model: M = handled.model @@ -166,7 +177,8 @@ case class ControllerClientEvent[M](handled: HandledEvent[M], event: ClientEvent case class HandledEvent[M]( model: M, componentsByKey: Map[String, UiElement], - shouldTerminate: Boolean + shouldTerminate: Boolean, + renderedChanges: Seq[UiElement] ): def terminate: HandledEvent[M] = copy(shouldTerminate = true) def withShouldTerminate(t: Boolean): HandledEvent[M] = copy(shouldTerminate = t) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/Keys.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/Keys.scala index 9dd161b3..c03346f6 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/Keys.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/Keys.scala @@ -2,17 +2,8 @@ package org.terminal21.client.components import org.terminal21.client.components.UiElement.HasChildren -object Keys: - def nextKey: String = "" +import java.util.concurrent.atomic.AtomicInteger - def linearKeys(parentKey: String, element: UiElement): Seq[UiElement] = linearKeys(parentKey, Seq(element)) - def linearKeys(elements: Seq[UiElement]): Seq[UiElement] = linearKeys(None, elements) - def linearKeys(parentKey: String, elements: Seq[UiElement]): Seq[UiElement] = linearKeys(Some(parentKey), elements) - def linearKeys(parentKey: Option[String], elements: Seq[UiElement]): Seq[UiElement] = - val pk = parentKey.map(_ + "-").getOrElse("k-") - elements.zipWithIndex.map: - case (e, i) => - val p = if e.key.isEmpty then e.withKey(s"$pk$i") else e - p match - case wc: HasChildren => wc.withChildren(linearKeys(Some(p.key), wc.children)*) - case n => n +object Keys: + private val id = new AtomicInteger(0) + def nextKey: String = "key-" + id.incrementAndGet() diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 9c28eeba..afd1af64 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -9,18 +9,18 @@ import org.terminal21.collections.SEList import org.terminal21.model.{ClientEvent, CommandEvent, OnChange, OnClick} class ControllerTest extends AnyFunSuiteLike: - val button = Button(key = "b1") + val button = Button() val buttonClick = OnClick(button.key) - val input = Input(key = "i1") + val input = Input() val inputChange = OnChange(input.key, "new-value") - val checkbox = Checkbox(key = "c1") + val checkbox = Checkbox() val checkBoxChange = OnChange(checkbox.key, "true") given ConnectedSession = ConnectedSessionMock.newConnectedSessionMock def newController[M]( initialModel: Model[M], events: => Seq[CommandEvent], - modelComponents: M => Seq[UiElement], + modelComponents: Seq[UiElement], renderChanges: Seq[UiElement] => Unit = _ => () ): Controller[M] = val seList = SEList[CommandEvent]() @@ -31,11 +31,11 @@ class ControllerTest extends AnyFunSuiteLike: test("will throw an exception if there is a duplicate key"): an[IllegalArgumentException] should be thrownBy - newController(Model(0), Seq(buttonClick), _ => Seq(button, button)).render().handledEventsIterator + newController(Model(0), Seq(buttonClick), Seq(button, button)).render().handledEventsIterator test("onEvent is called"): val model = Model(0) - newController(model, Seq(buttonClick), _ => Seq(button)) + newController(model, Seq(buttonClick), Seq(button)) .onEvent: event => if event.model > 1 then event.handled.terminate else event.handled.withModel(event.model + 1) .render() @@ -45,7 +45,7 @@ class ControllerTest extends AnyFunSuiteLike: test("onEvent is called for change"): val model = Model(0) - newController(model, Seq(inputChange), _ => Seq(input)) + newController(model, Seq(inputChange), Seq(input)) .onEvent: event => import event.* if event.model > 1 then handled.terminate else handled.withModel(event.model + 1) @@ -56,7 +56,7 @@ class ControllerTest extends AnyFunSuiteLike: test("onEvent not matched for change"): val model = Model(0) - newController(model, Seq(inputChange), _ => Seq(input)) + newController(model, Seq(inputChange), Seq(input)) .onEvent: case event: ControllerClickEvent[_] => import event.* @@ -68,7 +68,7 @@ class ControllerTest extends AnyFunSuiteLike: test("onEvent is called for change/boolean"): val model = Model(0) - newController(model, Seq(checkBoxChange), _ => Seq(checkbox)) + newController(model, Seq(checkBoxChange), Seq(checkbox)) .onEvent: event => import event.* if event.model > 1 then handled.terminate else handled.withModel(event.model + 1) @@ -79,7 +79,7 @@ class ControllerTest extends AnyFunSuiteLike: test("onEvent not matches for change/boolean"): val model = Model(0) - newController(model, Seq(checkBoxChange), _ => Seq(checkbox)) + newController(model, Seq(checkBoxChange), Seq(checkbox)) .onEvent: case event: ControllerClickEvent[_] => import event.* @@ -93,7 +93,7 @@ class ControllerTest extends AnyFunSuiteLike: test("onEvent is called for ClientEvent"): val model = Model(0) - newController(model, Seq(TestClientEvent(5)), _ => Seq(button)) + newController(model, Seq(TestClientEvent(5)), Seq(button)) .onEvent: case ControllerClientEvent(handled, event: TestClientEvent) => import event.* @@ -105,7 +105,7 @@ class ControllerTest extends AnyFunSuiteLike: test("onEvent when no partial function matches ClientEvent"): val model = Model(0) - newController(model, Seq(TestClientEvent(5)), _ => Seq(button)) + newController(model, Seq(TestClientEvent(5)), Seq(button)) .onEvent: case ControllerClickEvent(`checkbox`, handled) => handled.withModel(5).terminate @@ -119,11 +119,10 @@ class ControllerTest extends AnyFunSuiteLike: newController( model, Seq(buttonClick), - _ => - Seq( - button.onClick: event => - event.handled.withModel(100).terminate - ) + Seq( + button.onClick: event => + event.handled.withModel(100).terminate + ) ).render() .handledEventsIterator .map(_.model) @@ -134,11 +133,10 @@ class ControllerTest extends AnyFunSuiteLike: newController( model, Seq(inputChange), - _ => - Seq( - input.onChange: event => - event.handled.withModel(100).terminate - ) + Seq( + input.onChange: event => + event.handled.withModel(100).terminate + ) ).render() .handledEventsIterator .map(_.model) @@ -149,11 +147,10 @@ class ControllerTest extends AnyFunSuiteLike: newController( model, Seq(checkBoxChange), - _ => - Seq( - checkbox.onChange: event => - event.handled.withModel(100).terminate - ) + Seq( + checkbox.onChange: event => + event.handled.withModel(100).terminate + ) ).render() .handledEventsIterator .map(_.model) @@ -161,7 +158,7 @@ class ControllerTest extends AnyFunSuiteLike: test("terminate is obeyed and latest model state is iterated"): val model = Model(0) - newController(model, Seq(buttonClick, buttonClick, buttonClick), _ => Seq(button)) + newController(model, Seq(buttonClick, buttonClick, buttonClick), Seq(button)) .onEvent: event => if event.model > 1 then event.handled.terminate.withModel(100) else event.handled.withModel(event.model + 1) .render() @@ -170,20 +167,40 @@ class ControllerTest extends AnyFunSuiteLike: .toList should be(List(0, 1, 2, 100)) test("changes are rendered"): + given model: Model[Int] = Model(0) var rendered = Seq.empty[UiElement] def renderer(s: Seq[UiElement]): Unit = rendered = s - def components(m: Int) = Seq( - m match - case 0 => button - case 1 => button.withText("changed") - ) + val but = button.onModelChange: (b, m) => + b.withText(s"changed $m") - newController(Model(0), Seq(buttonClick), components, renderer) + val handled = newController(model, Seq(buttonClick), Seq(but), renderer) .onEvent: event => event.handled.withModel(event.model + 1).terminate .render() .handledEventsIterator - .map(_.model) - .toList should be(List(0, 1)) + .toList + + val expected = Seq(but.withText("changed 1")) + rendered should be(expected) + handled.map(_.renderedChanges)(1) should be(expected) + + test("rendered are cleared"): + given model: Model[Int] = Model(0) + var lastRendered = Seq.empty[UiElement] + def renderer(s: Seq[UiElement]): Unit = lastRendered = s + val but = button.onModelChange: (b, m) => + if m == 1 then b.withText(s"changed $m") else b + + val handled = newController(model, Seq(buttonClick, checkBoxChange), Seq(but, checkbox), renderer) + .onEvent: event => + val h = event.handled.withModel(event.model + 1) + if h.model > 1 then h.terminate else h + .render() + .handledEventsIterator + .toList - rendered should be(Seq(button.withText("changed"))) + lastRendered should be(Nil) + val rendered = handled.map(_.renderedChanges) + rendered.head should be(Nil) + rendered(1) should be(Seq(but.withText("changed 1"))) + rendered(2) should be(Nil) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/components/KeysTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/components/KeysTest.scala deleted file mode 100644 index 6fdbb948..00000000 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/components/KeysTest.scala +++ /dev/null @@ -1,22 +0,0 @@ -package org.terminal21.client.components - -import org.scalatest.funsuite.AnyFunSuiteLike -import org.terminal21.client.components.chakra.{Box, Text} -import org.scalatest.matchers.should.Matchers.* - -class KeysTest extends AnyFunSuiteLike: - test("doesn't reassign key to a defined key"): - val b = Box(key = "k1") - Keys.linearKeys(Seq(b)) should be(Seq(b)) - - test("assign key"): - val b = Box() - Keys.linearKeys(Seq(b)) should be(Seq(b.withKey("k-0"))) - - test("assign key to children"): - val b = Box().withChildren(Text(), Text()) - Keys.linearKeys(Seq(b)) should be( - Seq( - b.withKey("k-0").withChildren(Text().withKey("k-0-0"), Text().withKey("k-0-1")) - ) - ) From 43d197be624fa045f9ebe010c80d4224262c3454 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 29 Feb 2024 13:30:27 +0000 Subject: [PATCH 218/313] - --- .../serverapp/bundled/AppManager.scala | 2 +- .../serverapp/bundled/ServerStatusApp.scala | 36 +++++++++++-------- .../serverapp/bundled/SettingsApp.scala | 2 +- .../bundled/AppManagerPageTest.scala | 2 +- .../bundled/ServerStatusPageTest.scala | 36 +++++++------------ .../serverapp/bundled/SettingsPageTest.scala | 2 +- .../org/terminal21/client/Controller.scala | 3 +- .../client/components/UiComponent.scala | 2 +- .../client/components/UiElement.scala | 5 ++- 9 files changed, 43 insertions(+), 47 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala index 4e008084..aa9a3b49 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala @@ -39,7 +39,7 @@ class AppManagerPage(apps: Seq[ServerSideApp], startApp: ServerSideApp => Unit)( Text(text = app.description) ) - def components(m: ManagerModel): Seq[UiElement] = + def components: Seq[UiElement] = val appsTable = QuickTable( key = "apps-table", caption = Some("Apps installed on the server, click one to run it."), diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 345569e0..323651b0 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -25,7 +25,7 @@ class ServerStatusPage( sessionsService: ServerSessionsService )(using appSession: ConnectedSession, fiberExecutor: FiberExecutor): case class StatusModel(runtime: Runtime, sessions: Seq[Session]) - val initModel = StatusModel(Runtime.getRuntime, sessionsService.allSessions) + val initModel = StatusModel(Runtime.getRuntime, Nil) given Model[StatusModel] = Model(initModel) case class Ticker(sessions: Seq[Session]) extends ClientEvent @@ -47,8 +47,8 @@ class ServerStatusPage( case ControllerClientEvent(handled, Ticker(sessions)) => handled.withModel(handled.model.copy(sessions = sessions)) - def components(m: StatusModel): Seq[UiElement] = - Seq(jvmTable(m.runtime), sessionsTable(m.sessions)) + def components: Seq[UiElement] = + Seq(jvmTable, sessionsTable) private val jvmTableE = QuickTable(key = "jvmTable", caption = Some("JVM")) .withHeaders("Property", "Value", "Actions") @@ -57,15 +57,17 @@ class ServerStatusPage( System.gc() event.handled - def jvmTable(runtime: Runtime) = - jvmTableE.withRows( - Seq( - Seq("Free Memory", toMb(runtime.freeMemory()), ""), - Seq("Max Memory", toMb(runtime.maxMemory()), ""), - Seq("Total Memory", toMb(runtime.totalMemory()), gcButton), - Seq("Available processors", runtime.availableProcessors(), "") + def jvmTable: UiElement = + jvmTableE.onModelChange: (table, m) => + val runtime = m.runtime + table.withRows( + Seq( + Seq("Free Memory", toMb(runtime.freeMemory()), ""), + Seq("Max Memory", toMb(runtime.maxMemory()), ""), + Seq("Total Memory", toMb(runtime.totalMemory()), gcButton), + Seq("Available processors", runtime.availableProcessors(), "") + ) ) - ) private val sessionsTableE = QuickTable( @@ -73,10 +75,14 @@ class ServerStatusPage( caption = Some("All sessions") ).withHeaders("Id", "Name", "Is Open", "Actions") - def sessionsTable(sessions: Seq[Session]) = sessionsTableE.withRows( - sessions.map: session => - Seq(Text(text = session.id), Text(text = session.name), if session.isOpen then CheckIcon() else NotAllowedIcon(), actionsFor(session)) - ) + def sessionsTable: UiElement = + sessionsTableE.onModelChange: (table, m) => + val sessions = m.sessions + println("MODEL CHANGE") + table.withRows( + sessions.map: session => + Seq(Text(text = session.id), Text(text = session.name), if session.isOpen then CheckIcon() else NotAllowedIcon(), actionsFor(session)) + ) private def actionsFor(session: Session): UiElement = if session.isOpen then diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala index 8d102019..370870f3 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala @@ -24,6 +24,6 @@ class SettingsPage(using session: ConnectedSession): def run() = controller.render().handledEventsIterator.lastOption - def components(u: Unit) = Seq(themeToggle) + def components = Seq(themeToggle) def controller = Controller(components) diff --git a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala index 12f7c66e..a46e6a49 100644 --- a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala +++ b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala @@ -23,7 +23,7 @@ class AppManagerPageTest extends AnyFunSuiteLike: var startedApp: Option[ServerSideApp] = None val page = new AppManagerPage(apps, app => startedApp = Some(app)) val model = page.ManagerModel() - def allComponents = page.components(model).flatMap(_.flat) + def allComponents = page.components.flatMap(_.flat) test("renders app links"): new App(mockApp("app1", "the-app1-desc")): diff --git a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala index 3905d11c..cf59eaf0 100644 --- a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala +++ b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala @@ -1,16 +1,15 @@ package org.terminal21.serverapp.bundled -import org.mockito.Mockito.{verify, when} +import org.mockito.Mockito.when import org.scalatest.funsuite.AnyFunSuiteLike +import org.scalatest.matchers.should.Matchers.* import org.scalatestplus.mockito.MockitoSugar.mock -import org.terminal21.client.components.chakra.{Button, CheckIcon, NotAllowedIcon, Text} -import org.terminal21.client.{ConnectedSession, ConnectedSessionMock} +import org.terminal21.client.components.chakra.{Button, CheckIcon, NotAllowedIcon} +import org.terminal21.client.{ConnectedSession, ConnectedSessionMock, given} import org.terminal21.model.CommonModelBuilders.session import org.terminal21.model.{CommandEvent, CommonModelBuilders, Session} import org.terminal21.server.service.ServerSessionsService import org.terminal21.serverapp.ServerSideSessions -import org.terminal21.client.given -import org.scalatest.matchers.should.Matchers.* class ServerStatusPageTest extends AnyFunSuiteLike: class App: @@ -23,35 +22,32 @@ class ServerStatusPageTest extends AnyFunSuiteLike: test("Close button for a session"): new App: - page - .sessionsTable(Seq(session())) - .flat + page.sessionsTable.flat .collectFirst: case b: Button if b.text == "Close" => b .isEmpty should be(false) test("View state button for a session"): new App: - page - .sessionsTable(Seq(session())) - .flat + page.sessionsTable.flat .collectFirst: case b: Button if b.text == "View State" => b .isEmpty should be(false) test("When session is open, a CheckIcon is displayed"): new App: - page - .sessionsTable(Seq(session())) - .flat + page.sessionsTable.flat .collectFirst: case i: CheckIcon => i .isEmpty should be(false) test("When session is closed, a NotAllowedIcon is displayed"): new App: - page - .sessionsTable(Seq(session(isOpen = false))) + import page.given + val table = page.sessionsTable + val m = page.initModel.copy(sessions = Seq(session(isOpen = false))) + table + .fireModelChange(m) .flat .collectFirst: case i: NotAllowedIcon => i @@ -71,11 +67,3 @@ class ServerStatusPageTest extends AnyFunSuiteLike: handledEvents.head.model.sessions should be(Seq(session(id = "session1"))) handledEvents(1).model.sessions should be(sessions2) handledEvents(2).model.sessions should be(sessions3) - - test("closes session when close button is clicked"): - new App: - val it = page.controller.render().handledEventsIterator - connectedSession.fireClickEvent(page.sessionsTable(Seq(session1)).findKey("close-session1")) - connectedSession.fireSessionClosedEvent() - it.toList - verify(sessionsService).terminateAndRemove(session1) diff --git a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/SettingsPageTest.scala b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/SettingsPageTest.scala index a422c5d6..5a88f916 100644 --- a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/SettingsPageTest.scala +++ b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/SettingsPageTest.scala @@ -11,4 +11,4 @@ class SettingsPageTest extends AnyFunSuiteLike: test("Should render the ThemeToggle component"): new App: - page.components(()) should contain(page.themeToggle) + page.components should contain(page.themeToggle) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 3246cd21..9195a985 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -115,7 +115,6 @@ class RenderedController[M]( ) private def doRenderChanges(oldHandled: HandledEvent[M], newHandled: HandledEvent[M]): HandledEvent[M] = - // TODO: optimise what elements are rendered val changeFunctions = for e <- newHandled.componentsByKey.values @@ -129,6 +128,7 @@ class RenderedController[M]( .filter: (e, ne) => e.withDataStore(dsEmpty) != ne.withDataStore(dsEmpty) .map(_._2) + .map(_.substituteComponents) .toList renderChanges(changed) newHandled.copy(componentsByKey = calcComponentsByKeyMap(changed), renderedChanges = changed) @@ -140,7 +140,6 @@ class RenderedController[M]( .takeWhile(!_.isSessionClosed) .scanLeft((initHandled, initHandled)): case ((_, oldHandled), event) => - println(event) try val handled2 = invokeEventHandlers(oldHandled, event) val handled3 = invokeComponentEventHandlers(handled2, event) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiComponent.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiComponent.scala index 483f21a3..473f992b 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiComponent.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiComponent.scala @@ -8,4 +8,4 @@ trait UiComponent extends UiElement: def rendered: Seq[UiElement] override def flat = Seq(this) ++ rendered.flatMap(_.flat) - protected def subKey(suffix: String): String = if key.isEmpty then "" else key + "-" + suffix + protected def subKey(suffix: String): String = key + "-" + suffix diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala index 27551615..5951870a 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala @@ -19,6 +19,9 @@ abstract class UiElement extends AnyElement: def onModelChange[M](using model: Model[M])(f: (This, M) => This): This = store(model.OnModelChangeKey, f.asInstanceOf[model.OnModelChangeFunction]) + def fireModelChange[M](using model: Model[M])(m: M) = + dataStore(model.OnModelChangeKey).apply(this, m) + /** @return * this element along all it's children flattened */ @@ -26,7 +29,7 @@ abstract class UiElement extends AnyElement: def substituteComponents: UiElement = this match - case c: UiComponent => Box(key = c.key, text = "", children = c.rendered.map(_.substituteComponents)) + case c: UiComponent => Box(key = c.key, text = "", children = c.rendered.map(_.substituteComponents), dataStore = c.dataStore) case ch: HasChildren => ch.withChildren(ch.children.map(_.substituteComponents)*) case _ => this From eb1ccb7878eb2f0001fcaf62d9d9178260856df1 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 29 Feb 2024 13:43:12 +0000 Subject: [PATCH 219/313] - --- .../terminal21/client/ConnectedSession.scala | 9 ++-- .../org/terminal21/client/Controller.scala | 6 +-- .../terminal21/client/ControllerTest.scala | 45 ++++++++++++++++++- 3 files changed, 51 insertions(+), 9 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index 41c65c77..a70e3306 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -107,10 +107,11 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se val j = toJson(es) sessionsService.changeSessionJsonState(session, j) - private def toJson(elements: Seq[UiElement]): ServerJson = - val root = Box(key = "root", children = elements) // keep the root element with a steady key - val flat = root.flat - val sj = ServerJson( + private def toJson(elementsUn: Seq[UiElement]): ServerJson = + val elements = elementsUn.map(_.substituteComponents) + val root = Box(key = "root", children = elements) // keep the root element with a steady key + val flat = root.flat + val sj = ServerJson( Seq(root.key), flat .map: el => diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 9195a985..391dbf7f 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -17,9 +17,8 @@ class Controller[M]( ): def render()(using session: ConnectedSession): RenderedController[M] = - val initComponents = modelComponents.map(_.substituteComponents) - session.render(initComponents) - new RenderedController(eventIteratorFactory, initialModel, initComponents, renderChanges, eventHandlers) + session.render(modelComponents) + new RenderedController(eventIteratorFactory, initialModel, modelComponents, renderChanges, eventHandlers) def onEvent(handler: PartialFunction[ControllerEvent[M], HandledEvent[M]]) = new Controller( @@ -128,7 +127,6 @@ class RenderedController[M]( .filter: (e, ne) => e.withDataStore(dsEmpty) != ne.withDataStore(dsEmpty) .map(_._2) - .map(_.substituteComponents) .toList renderChanges(changed) newHandled.copy(componentsByKey = calcComponentsByKeyMap(changed), renderedChanges = changed) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index afd1af64..d017b6da 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -3,11 +3,13 @@ package org.terminal21.client import org.scalatest.funsuite.AnyFunSuiteLike import org.scalatest.matchers.should.Matchers.* import org.terminal21.client.components.UiElement -import org.terminal21.client.components.chakra.{Button, Checkbox} +import org.terminal21.client.components.chakra.{Button, Checkbox, QuickTable} import org.terminal21.client.components.std.Input import org.terminal21.collections.SEList import org.terminal21.model.{ClientEvent, CommandEvent, OnChange, OnClick} +import java.util.concurrent.atomic.AtomicBoolean + class ControllerTest extends AnyFunSuiteLike: val button = Button() val buttonClick = OnClick(button.key) @@ -204,3 +206,44 @@ class ControllerTest extends AnyFunSuiteLike: rendered.head should be(Nil) rendered(1) should be(Seq(but.withText("changed 1"))) rendered(2) should be(Nil) + + test("components handle events"): + given m: Model[Int] = Model(0) + val table = QuickTable().withRows( + Seq( + Seq( + button.onClick: event => + import event.* + handled.withModel(model + 1).terminate + ) + ) + ) + val handledEvents = newController(m, Seq(buttonClick), Seq(table)) + .render() + .handledEventsIterator + .toList + + handledEvents.map(_.model) should be(List(0, 1)) + + test("components receive onModelChange"): + given m: Model[Int] = Model(0) + val called = new AtomicBoolean(false) + val table = QuickTable() + .withRows( + Seq( + Seq( + button.onClick: event => + import event.* + handled.withModel(model + 1).terminate + ) + ) + ) + .onModelChange: (table, m) => + called.set(true) + table + val handledEvents = newController(m, Seq(buttonClick), Seq(table)) + .render() + .handledEventsIterator + .toList + + called.get() should be(true) From 8c9b83c8b82e4a15523d9381bf569320c7acbff4 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 29 Feb 2024 13:52:41 +0000 Subject: [PATCH 220/313] - --- .../org/terminal21/serverapp/bundled/ServerStatusApp.scala | 1 + .../main/scala/org/terminal21/client/ConnectedSession.scala | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 323651b0..4c29fe64 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -4,6 +4,7 @@ import functions.fibers.FiberExecutor import org.terminal21.client.* import org.terminal21.client.components.* import org.terminal21.client.components.chakra.* +import org.terminal21.client.components.std.Paragraph import org.terminal21.model.{ClientEvent, Session} import org.terminal21.server.Dependencies import org.terminal21.server.model.SessionState diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index a70e3306..8c12b244 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -109,10 +109,9 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se private def toJson(elementsUn: Seq[UiElement]): ServerJson = val elements = elementsUn.map(_.substituteComponents) - val root = Box(key = "root", children = elements) // keep the root element with a steady key - val flat = root.flat + val flat = elements.flatMap(_.flat) val sj = ServerJson( - Seq(root.key), + elements.map(_.key), flat .map: el => ( From 49a248205165d1ab93ad08f590489636cdde9283 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 29 Feb 2024 14:09:03 +0000 Subject: [PATCH 221/313] - --- .../serverapp/bundled/ServerStatusApp.scala | 1 - .../org/terminal21/client/Controller.scala | 19 +++++++++---------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 4c29fe64..ce2cee0b 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -79,7 +79,6 @@ class ServerStatusPage( def sessionsTable: UiElement = sessionsTableE.onModelChange: (table, m) => val sessions = m.sessions - println("MODEL CHANGE") table.withRows( sessions.map: session => Seq(Text(text = session.id), Text(text = session.name), if session.isOpen then CheckIcon() else NotAllowedIcon(), actionsFor(session)) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 391dbf7f..2d9db422 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -113,7 +113,7 @@ class RenderedController[M]( ) ) - private def doRenderChanges(oldHandled: HandledEvent[M], newHandled: HandledEvent[M]): HandledEvent[M] = + private def doRenderChanges(newHandled: HandledEvent[M]): HandledEvent[M] = val changeFunctions = for e <- newHandled.componentsByKey.values @@ -129,25 +129,24 @@ class RenderedController[M]( .map(_._2) .toList renderChanges(changed) - newHandled.copy(componentsByKey = calcComponentsByKeyMap(changed), renderedChanges = changed) + newHandled.copy(componentsByKey = newHandled.componentsByKey ++ calcComponentsByKeyMap(changed), renderedChanges = changed) def handledEventsIterator: EventIterator[HandledEvent[M]] = val initHandled = HandledEvent(initialModel.value, calcComponentsByKeyMap(initialComponents), false, Nil) new EventIterator( eventIteratorFactory .takeWhile(!_.isSessionClosed) - .scanLeft((initHandled, initHandled)): - case ((_, oldHandled), event) => + .scanLeft(initHandled): + case (oldHandled, event) => try - val handled2 = invokeEventHandlers(oldHandled, event) - val handled3 = invokeComponentEventHandlers(handled2, event) - (oldHandled, handled3) + val handled2 = invokeEventHandlers(oldHandled, event) + val handled3 = invokeComponentEventHandlers(handled2, event) + val newHandled = if oldHandled.model != handled3.model then doRenderChanges(handled3) else handled3.copy(renderedChanges = Nil) + newHandled catch case t: Throwable => logger.error("an error occurred while iterating events", t) - (oldHandled, oldHandled) - .map: (oldHandled, newHandled) => - if oldHandled.model != newHandled.model then doRenderChanges(oldHandled, newHandled) else newHandled.copy(renderedChanges = Nil) + oldHandled .flatMap: h => // trick to make sure we take the last state of the model when shouldTerminate=true if h.shouldTerminate then Seq(h.copy(shouldTerminate = false), h) else Seq(h) From 1c19737e3813d9a541c7abe1ace99eae58ebb813 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 29 Feb 2024 14:42:49 +0000 Subject: [PATCH 222/313] - --- .../src/main/scala/tests/ChakraComponents.scala | 2 +- .../src/main/scala/tests/LoginPage.scala | 10 ++++++---- .../main/scala/tests/StateSessionStateBug.scala | 4 ++-- .../src/main/scala/tests/StdComponents.scala | 4 ++-- .../src/test/scala/tests/LoggedInTest.scala | 2 +- .../src/test/scala/tests/LoginPageTest.scala | 15 ++++----------- 6 files changed, 16 insertions(+), 21 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala index 38bd5694..8caf2b64 100644 --- a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala @@ -30,7 +30,7 @@ import tests.chakra.* Navigation.components(m) ++ Seq( krButton ) - Controller(components).render().handledEventsIterator.lastOption.map(_.model) match + Controller(components(model.value)).render().handledEventsIterator.lastOption.map(_.model) match case Some(m) if m.rerun => Controller.noModel(Seq(Paragraph(text = "chakra-session-reset"))).render() Thread.sleep(500) diff --git a/end-to-end-tests/src/main/scala/tests/LoginPage.scala b/end-to-end-tests/src/main/scala/tests/LoginPage.scala index 75a13c9b..affdfa8e 100644 --- a/end-to-end-tests/src/main/scala/tests/LoginPage.scala +++ b/end-to-end-tests/src/main/scala/tests/LoginPage.scala @@ -56,7 +56,7 @@ class LoginPage(using session: ConnectedSession): .dropWhile(!_.submitted) .nextOption() - def components(loginForm: LoginForm): Seq[UiElement] = + def components: Seq[UiElement] = Seq( QuickFormControl() .withLabel("Email address") @@ -64,7 +64,8 @@ class LoginPage(using session: ConnectedSession): .withInputGroup( InputLeftAddon().withChildren(EmailIcon()), emailInput, - InputRightAddon().withChildren(if loginForm.isValidEmail then okIcon else notOkIcon) + InputRightAddon().onModelChange: (i, m) => + i.withChildren(if m.isValidEmail then okIcon else notOkIcon) ), QuickFormControl() .withLabel("Password") @@ -74,7 +75,8 @@ class LoginPage(using session: ConnectedSession): passwordInput ), submitButton, - if loginForm.submittedInvalidEmail then errorsBox.withChildren(errorMsgInvalidEmail) else errorsBox + errorsBox.onModelChange: (eb, m) => + if m.submittedInvalidEmail then eb.withChildren(errorMsgInvalidEmail) else errorsBox ) def controller: Controller[LoginForm] = Controller(components) @@ -99,7 +101,7 @@ class LoggedIn(login: LoginForm)(using session: ConnectedSession): def run(): Option[Boolean] = controller.render().handledEventsIterator.lastOption.map(_.model) - def components(m: Boolean): Seq[UiElement] = + def components: Seq[UiElement] = Seq( Paragraph().withChildren( Text(text = "Are your details correct?"), diff --git a/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala b/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala index 44b7b52a..672ada6c 100644 --- a/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala +++ b/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala @@ -14,8 +14,8 @@ import java.util.Date given ConnectedSession = session import Model.Standard.unitModel - val date = new Date() - def components(m: Unit) = Seq( + val date = new Date() + val components = Seq( Paragraph(text = s"Now: $date"), QuickTable() .withHeaders("Title", "Value") diff --git a/end-to-end-tests/src/main/scala/tests/StdComponents.scala b/end-to-end-tests/src/main/scala/tests/StdComponents.scala index d9d55bf6..3913e808 100644 --- a/end-to-end-tests/src/main/scala/tests/StdComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/StdComponents.scala @@ -11,7 +11,7 @@ import org.terminal21.client.components.std.* given ConnectedSession = session case class Form(output: String, cookie: String) - given Model[Form] = Model(Form("This will reflect what you type in the input", "This will display the value of the cookie")) + given model: Model[Form] = Model(Form("This will reflect what you type in the input", "This will display the value of the cookie")) def components(form: Form) = val output = Paragraph(text = form.output) @@ -44,4 +44,4 @@ import org.terminal21.client.components.std.* cookieValue ) - Controller(components).render().handledEventsIterator.lastOption + Controller(components(model.value)).render().handledEventsIterator.lastOption diff --git a/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala b/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala index 85fc992f..c6f2a1a1 100644 --- a/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala +++ b/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala @@ -10,7 +10,7 @@ class LoggedInTest extends AnyFunSuiteLike: val login = LoginForm() given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock val form = new LoggedIn(login) - def allComponents = form.components(false).flatMap(_.flat) + def allComponents = form.components.flatMap(_.flat) test("renders email details"): new App: diff --git a/end-to-end-tests/src/test/scala/tests/LoginPageTest.scala b/end-to-end-tests/src/test/scala/tests/LoginPageTest.scala index 090ffb30..7b479973 100644 --- a/end-to-end-tests/src/test/scala/tests/LoginPageTest.scala +++ b/end-to-end-tests/src/test/scala/tests/LoginPageTest.scala @@ -9,11 +9,10 @@ import org.terminal21.model.CommandEvent class LoginPageTest extends AnyFunSuiteLike: class App: - given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock - val login = LoginForm() - val page = new LoginPage - def allComponents: Seq[UiElement] = allComponents(login) - def allComponents(form: LoginForm): Seq[UiElement] = page.components(form).flatMap(_.flat) + given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock + val login = LoginForm() + val page = new LoginPage + def allComponents: Seq[UiElement] = page.components.flatMap(_.flat) test("renders email input"): new App: @@ -38,9 +37,3 @@ class LoginPageTest extends AnyFunSuiteLike: ) eventsIt.lastOption.map(_.model) should be(Some(LoginForm("an@email.com", "secret", true))) - - test("user submits invalid email"): - new App: - val all = allComponents(login.copy(email = "invalid.com", submitted = false, submittedInvalidEmail = true)) - all should contain(page.notOkIcon) - all should contain(page.errorsBox.withChildren(page.errorMsgInvalidEmail)) From 2817286248e2f8b676fa34dad56d76cc3e77a2bc Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 29 Feb 2024 14:49:44 +0000 Subject: [PATCH 223/313] - --- .../src/main/scala/tests/chakra/DataDisplay.scala | 5 ++--- .../scala/org/terminal21/collections/TypedMap.scala | 1 + .../scala/org/terminal21/client/Controller.scala | 12 +++++++----- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/chakra/DataDisplay.scala b/end-to-end-tests/src/main/scala/tests/chakra/DataDisplay.scala index 2ce70a17..cec78561 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/DataDisplay.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/DataDisplay.scala @@ -1,14 +1,13 @@ package tests.chakra -import org.terminal21.client.ConnectedSession import org.terminal21.client.components.UiElement import org.terminal21.client.components.chakra.* import org.terminal21.client.components.std.NewLine import tests.chakra.Common.* object DataDisplay: - def components(using session: ConnectedSession): Seq[UiElement] = - val headAndFoot = Tr().withChildren( + def components: Seq[UiElement] = + def headAndFoot = Tr().withChildren( Th(text = "To convert"), Th(text = "into"), Th(text = "multiply by", isNumeric = true) diff --git a/terminal21-server-client-common/src/main/scala/org/terminal21/collections/TypedMap.scala b/terminal21-server-client-common/src/main/scala/org/terminal21/collections/TypedMap.scala index 6ede3e03..0de17d70 100644 --- a/terminal21-server-client-common/src/main/scala/org/terminal21/collections/TypedMap.scala +++ b/terminal21-server-client-common/src/main/scala/org/terminal21/collections/TypedMap.scala @@ -12,6 +12,7 @@ class TypedMap(val m: Map[TypedMapKey[_], Any]): case _ => false def contains[A](k: TypedMapKey[A]) = m.contains(k) + override def toString = s"TypedMap(${m.keys.mkString(", ")})" object TypedMap: def empty = new TypedMap(Map.empty) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 2d9db422..47b3832b 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -4,7 +4,7 @@ import org.slf4j.LoggerFactory import org.terminal21.client.collections.EventIterator import org.terminal21.client.components.OnChangeEventHandler.CanHandleOnChangeEvent import org.terminal21.client.components.OnClickEventHandler.CanHandleOnClickEvent -import org.terminal21.client.components.{Keys, OnChangeBooleanEventHandler, OnChangeEventHandler, OnClickEventHandler, UiElement} +import org.terminal21.client.components.{Keys, OnChangeBooleanEventHandler, OnChangeEventHandler, OnClickEventHandler, UiComponent, UiElement} import org.terminal21.collections.{TypedMap, TypedMapKey} import org.terminal21.model.{ClientEvent, CommandEvent, OnChange, OnClick} @@ -96,14 +96,16 @@ class RenderedController[M]( handled case _ => h - private def checkForDuplicatesAndThrow(seq: Seq[String]): Unit = - val duplicates = seq.groupBy(identity).filter(_._2.size > 1).keys.toList - if duplicates.nonEmpty then throw new IllegalArgumentException(s"Duplicate(s) found: ${duplicates.mkString(", ")}") + private def checkForDuplicatesAndThrow(components: Seq[UiElement]): Unit = + val duplicates = components.map(_.key).groupBy(identity).filter(_._2.size > 1).keys.toSet + if duplicates.nonEmpty then + val duplicateComponents = components.filter(e => duplicates.contains(e.key)) + throw new IllegalArgumentException(s"Duplicate(s) found: ${duplicates.mkString(", ")}\nDuplicate components:\n${duplicateComponents.mkString("\n")}") private def calcComponentsByKeyMap(components: Seq[UiElement]): Map[String, UiElement] = val flattened = components .flatMap(_.flat) - checkForDuplicatesAndThrow(flattened.map(_.key)) + checkForDuplicatesAndThrow(flattened) val all = flattened .map(c => (c.key, c)) .toMap From a2bac5b387b4f06f9ece904022c150179f6868d0 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 29 Feb 2024 15:07:26 +0000 Subject: [PATCH 224/313] - --- .../src/main/scala/tests/chakra/Overlay.scala | 3 ++- .../serverapp/bundled/ServerStatusApp.scala | 3 +-- .../scala/org/terminal21/client/Controller.scala | 15 ++++++++++++--- .../terminal21/client/components/UiElement.scala | 4 ++-- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala b/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala index dc1e06d5..a0e1fb4d 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala @@ -7,7 +7,8 @@ import tests.chakra.Common.commonBox object Overlay: def components(m: ChakraModel)(using Model[ChakraModel]): Seq[UiElement] = - val box1 = Box(text = m.box1) + val box1 = Box(text = "initializing...").onModelChange: (b, m) => + b.withText(m.box1) Seq( commonBox(text = "Menus box0001"), HStack().withChildren( diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index ce2cee0b..75d6d282 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -4,7 +4,6 @@ import functions.fibers.FiberExecutor import org.terminal21.client.* import org.terminal21.client.components.* import org.terminal21.client.components.chakra.* -import org.terminal21.client.components.std.Paragraph import org.terminal21.model.{ClientEvent, Session} import org.terminal21.server.Dependencies import org.terminal21.server.model.SessionState @@ -26,7 +25,7 @@ class ServerStatusPage( sessionsService: ServerSessionsService )(using appSession: ConnectedSession, fiberExecutor: FiberExecutor): case class StatusModel(runtime: Runtime, sessions: Seq[Session]) - val initModel = StatusModel(Runtime.getRuntime, Nil) + val initModel = StatusModel(Runtime.getRuntime, sessionsService.allSessions) given Model[StatusModel] = Model(initModel) case class Ticker(sessions: Seq[Session]) extends ClientEvent diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 47b3832b..7dbb994c 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -4,7 +4,8 @@ import org.slf4j.LoggerFactory import org.terminal21.client.collections.EventIterator import org.terminal21.client.components.OnChangeEventHandler.CanHandleOnChangeEvent import org.terminal21.client.components.OnClickEventHandler.CanHandleOnClickEvent -import org.terminal21.client.components.{Keys, OnChangeBooleanEventHandler, OnChangeEventHandler, OnClickEventHandler, UiComponent, UiElement} +import org.terminal21.client.components.UiElement.HasChildren +import org.terminal21.client.components.{OnChangeBooleanEventHandler, OnChangeEventHandler, OnClickEventHandler, UiElement} import org.terminal21.collections.{TypedMap, TypedMapKey} import org.terminal21.model.{ClientEvent, CommandEvent, OnChange, OnClick} @@ -16,9 +17,17 @@ class Controller[M]( eventHandlers: Seq[PartialFunction[ControllerEvent[M], HandledEvent[M]]] ): + private def applyModelTo(components: Seq[UiElement]): Seq[UiElement] = + components.map: e => + val ne = if e.hasModelChangeHandler(using initialModel) then e.fireModelChange(using initialModel)(initialModel.value) else e + ne match + case ch: HasChildren => ch.withChildren(applyModelTo(ch.children)*) + case x => x + def render()(using session: ConnectedSession): RenderedController[M] = - session.render(modelComponents) - new RenderedController(eventIteratorFactory, initialModel, modelComponents, renderChanges, eventHandlers) + val elements = applyModelTo(modelComponents) + session.render(elements) + new RenderedController(eventIteratorFactory, initialModel, elements, renderChanges, eventHandlers) def onEvent(handler: PartialFunction[ControllerEvent[M], HandledEvent[M]]) = new Controller( diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala index 5951870a..337cc097 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala @@ -18,8 +18,8 @@ abstract class UiElement extends AnyElement: def onModelChange[M](using model: Model[M])(f: (This, M) => This): This = store(model.OnModelChangeKey, f.asInstanceOf[model.OnModelChangeFunction]) - - def fireModelChange[M](using model: Model[M])(m: M) = + def hasModelChangeHandler[M](using model: Model[M]): Boolean = dataStore.contains(model.OnModelChangeKey) + def fireModelChange[M](using model: Model[M])(m: M) = dataStore(model.OnModelChangeKey).apply(this, m) /** @return From 252f26b66c694ca2da037ebe8609d492c91fd54e Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 29 Feb 2024 15:18:23 +0000 Subject: [PATCH 225/313] - --- build.sbt | 1 + .../client/components/UiElement.scala | 2 ++ .../terminal21/client/ControllerTest.scala | 21 ++++++++++++++++--- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/build.sbt b/build.sbt index fff0aa64..42f5a94b 100644 --- a/build.sbt +++ b/build.sbt @@ -141,6 +141,7 @@ lazy val `terminal21-ui-std` = project libraryDependencies ++= Seq( ScalaTest, Mockito, + Mockito510, Slf4jApi, HelidonClient, FunctionsCaller, diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala index 337cc097..7c946b62 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala @@ -16,6 +16,8 @@ abstract class UiElement extends AnyElement: def withDataStore(ds: TypedMap): This def store[V](key: TypedMapKey[V], value: V): This = withDataStore(dataStore + (key -> value)) + /** This handler will be called whenever the model changes. It will also be called with the initial model before the first render() + */ def onModelChange[M](using model: Model[M])(f: (This, M) => This): This = store(model.OnModelChangeKey, f.asInstanceOf[model.OnModelChangeFunction]) def hasModelChangeHandler[M](using model: Model[M]): Boolean = dataStore.contains(model.OnModelChangeKey) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index d017b6da..524de8ad 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -1,7 +1,10 @@ package org.terminal21.client +import org.mockito.Mockito +import org.mockito.Mockito.verify import org.scalatest.funsuite.AnyFunSuiteLike import org.scalatest.matchers.should.Matchers.* +import org.scalatestplus.mockito.MockitoSugar.* import org.terminal21.client.components.UiElement import org.terminal21.client.components.chakra.{Button, Checkbox, QuickTable} import org.terminal21.client.components.std.Input @@ -238,12 +241,24 @@ class ControllerTest extends AnyFunSuiteLike: ) ) ) - .onModelChange: (table, m) => + .onModelChange: (table, _) => called.set(true) table - val handledEvents = newController(m, Seq(buttonClick), Seq(table)) + newController(m, Seq(buttonClick), Seq(table)) .render() .handledEventsIterator - .toList + .lastOption called.get() should be(true) + + test("applies initial model before rendering"): + given m: Model[Int] = Model(5) + + val b = button.onModelChange: (b, m) => + b.withText(s"model $m") + + val connectedSession = mock[ConnectedSession] + newController(m, Nil, Seq(b)) + .render()(using connectedSession) + + verify(connectedSession).render(Seq(b.withText("model 5"))) From a701b1debd95cdb2e9d1054696ab1c51c71116d6 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 29 Feb 2024 15:39:22 +0000 Subject: [PATCH 226/313] - --- end-to-end-tests/src/main/scala/tests/chakra/Forms.scala | 6 ++++-- end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala b/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala index 4b1d51cd..365541c9 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala @@ -7,11 +7,13 @@ import tests.chakra.Common.* object Forms: def components(m: ChakraModel)(using Model[ChakraModel]): Seq[UiElement] = - val status = Box(text = m.formStatus) + val status = Box().onModelChange: (b, m) => + b.withText(m.formStatus) val okIcon = CheckCircleIcon(color = Some("green")) val notOkIcon = WarningTwoIcon(color = Some("red")) - val emailRightAddOn = InputRightAddon().withChildren(if m.email.contains("@") then okIcon else notOkIcon) + val emailRightAddOn = InputRightAddon().onModelChange: (i, m) => + i.withChildren(if m.email.contains("@") then okIcon else notOkIcon) val email = Input(key = "email", `type` = "email", defaultValue = m.email) .onChange: event => diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala b/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala index a0e1fb4d..803f1c65 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala @@ -7,7 +7,7 @@ import tests.chakra.Common.commonBox object Overlay: def components(m: ChakraModel)(using Model[ChakraModel]): Seq[UiElement] = - val box1 = Box(text = "initializing...").onModelChange: (b, m) => + val box1 = Box().onModelChange: (b, m) => b.withText(m.box1) Seq( commonBox(text = "Menus box0001"), From f5f085e87852b76d2212afcae5cdee880a76cdb8 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 29 Feb 2024 16:58:06 +0000 Subject: [PATCH 227/313] - --- .../src/main/scala/tests/ChakraComponents.scala | 6 ++---- .../src/main/scala/tests/StdComponents.scala | 12 +++++++----- .../src/main/scala/tests/chakra/Editables.scala | 7 ++++--- .../src/main/scala/tests/chakra/Navigation.scala | 8 +++++--- project/build.properties | 2 +- 5 files changed, 19 insertions(+), 16 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala index 8caf2b64..e5617518 100644 --- a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala @@ -24,10 +24,8 @@ import tests.chakra.* m ) ++ Forms.components( m - ) ++ Editables.components( - m - ) ++ Stacks.components ++ Grids.components ++ Buttons.components ++ Etc.components ++ MediaAndIcons.components ++ DataDisplay.components ++ Typography.components ++ Feedback.components ++ Disclosure.components ++ - Navigation.components(m) ++ Seq( + ) ++ Editables.components ++ Stacks.components ++ Grids.components ++ Buttons.components ++ Etc.components ++ MediaAndIcons.components ++ DataDisplay.components ++ Typography.components ++ Feedback.components ++ Disclosure.components ++ + Navigation.components ++ Seq( krButton ) Controller(components(model.value)).render().handledEventsIterator.lastOption.map(_.model) match diff --git a/end-to-end-tests/src/main/scala/tests/StdComponents.scala b/end-to-end-tests/src/main/scala/tests/StdComponents.scala index 3913e808..256296dc 100644 --- a/end-to-end-tests/src/main/scala/tests/StdComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/StdComponents.scala @@ -13,12 +13,14 @@ import org.terminal21.client.components.std.* case class Form(output: String, cookie: String) given model: Model[Form] = Model(Form("This will reflect what you type in the input", "This will display the value of the cookie")) - def components(form: Form) = - val output = Paragraph(text = form.output) - val cookieValue = Paragraph(text = form.cookie) + def components = + val output = Paragraph().onModelChange: (p, m) => + p.withText(m.output) + val cookieValue = Paragraph().onModelChange: (p, m) => + p.withText(m.cookie) val input = Input(key = "name", defaultValue = "Please enter your name").onChange: event => import event.* - handled.withModel(form.copy(output = newValue)) + handled.withModel(event.model.copy(output = newValue)) Seq( Header1(text = "header1 test"), @@ -44,4 +46,4 @@ import org.terminal21.client.components.std.* cookieValue ) - Controller(components(model.value)).render().handledEventsIterator.lastOption + Controller(components).render().handledEventsIterator.lastOption diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala b/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala index 2b41f34a..b8d2c728 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala @@ -1,13 +1,14 @@ package tests.chakra -import org.terminal21.client.{ConnectedSession, Model} +import org.terminal21.client.Model import org.terminal21.client.components.UiElement import org.terminal21.client.components.chakra.* import tests.chakra.Common.* object Editables: - def components(model: ChakraModel)(using Model[ChakraModel]): Seq[UiElement] = - val status = Box(text = model.editableStatus) + def components(using Model[ChakraModel]): Seq[UiElement] = + val status = Box().onModelChange: (b, m) => + b.withText(m.editableStatus) val editable1 = Editable(key = "editable1", defaultValue = "Please type here") .withChildren( diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Navigation.scala b/end-to-end-tests/src/main/scala/tests/chakra/Navigation.scala index ff8efeec..d772ed75 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Navigation.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Navigation.scala @@ -7,11 +7,13 @@ import org.terminal21.client.components.std.Paragraph import tests.chakra.Common.commonBox object Navigation: - def components(m: ChakraModel)(using Model[ChakraModel]): Seq[UiElement] = - val clickedBreadcrumb = Paragraph(text = m.breadcrumbStatus) + def components(using Model[ChakraModel]): Seq[UiElement] = + val clickedBreadcrumb = Paragraph().onModelChange: (p, m) => + p.withText(m.breadcrumbStatus) def breadcrumbClicked(m: ChakraModel, t: String) = m.copy(breadcrumbStatus = s"breadcrumb-click: $t") - val clickedLink = Paragraph(text = m.linkStatus) + val clickedLink = Paragraph().onModelChange: (p, m) => + p.withText(m.linkStatus) Seq( commonBox(text = "Breadcrumbs"), diff --git a/project/build.properties b/project/build.properties index abbbce5d..04267b14 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.8 +sbt.version=1.9.9 From c5971e3ceb99bbbbc8d9d0a944d8a8b36edd2084 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 29 Feb 2024 17:01:10 +0000 Subject: [PATCH 228/313] - --- end-to-end-tests/src/main/scala/tests/ChakraComponents.scala | 4 +--- end-to-end-tests/src/main/scala/tests/chakra/Disclosure.scala | 3 +-- end-to-end-tests/src/main/scala/tests/chakra/Etc.scala | 3 +-- end-to-end-tests/src/main/scala/tests/chakra/Feedback.scala | 3 +-- end-to-end-tests/src/main/scala/tests/chakra/Grids.scala | 1 - .../src/main/scala/tests/chakra/MediaAndIcons.scala | 2 +- end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala | 2 +- end-to-end-tests/src/main/scala/tests/chakra/Stacks.scala | 3 +-- 8 files changed, 7 insertions(+), 14 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala index e5617518..10c50de9 100644 --- a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala @@ -20,9 +20,7 @@ import tests.chakra.* event.handled.withModel(_.copy(rerun = true)).terminate def components(m: ChakraModel): Seq[UiElement] = - Overlay.components( - m - ) ++ Forms.components( + Overlay.components ++ Forms.components( m ) ++ Editables.components ++ Stacks.components ++ Grids.components ++ Buttons.components ++ Etc.components ++ MediaAndIcons.components ++ DataDisplay.components ++ Typography.components ++ Feedback.components ++ Disclosure.components ++ Navigation.components ++ Seq( diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Disclosure.scala b/end-to-end-tests/src/main/scala/tests/chakra/Disclosure.scala index 27e1ca65..2f2c16d2 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Disclosure.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Disclosure.scala @@ -1,13 +1,12 @@ package tests.chakra -import org.terminal21.client.ConnectedSession import org.terminal21.client.components.UiElement import org.terminal21.client.components.chakra.* import org.terminal21.client.components.std.Paragraph import tests.chakra.Common.commonBox object Disclosure: - def components(using session: ConnectedSession): Seq[UiElement] = + def components: Seq[UiElement] = Seq( commonBox(text = "Tabs"), Tabs().withChildren( diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Etc.scala b/end-to-end-tests/src/main/scala/tests/chakra/Etc.scala index 42b930c1..76b83037 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Etc.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Etc.scala @@ -1,12 +1,11 @@ package tests.chakra -import org.terminal21.client.ConnectedSession import org.terminal21.client.components.UiElement import org.terminal21.client.components.chakra.* import tests.chakra.Common.* object Etc: - def components(using session: ConnectedSession): Seq[UiElement] = + def components: Seq[UiElement] = Seq( commonBox(text = "Center"), Center(text = "Center demo, not styled"), diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Feedback.scala b/end-to-end-tests/src/main/scala/tests/chakra/Feedback.scala index 72fd6f7f..ba9c2749 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Feedback.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Feedback.scala @@ -1,12 +1,11 @@ package tests.chakra -import org.terminal21.client.ConnectedSession import org.terminal21.client.components.UiElement import org.terminal21.client.components.chakra.* import tests.chakra.Common.commonBox object Feedback: - def components(using session: ConnectedSession): Seq[UiElement] = + def components: Seq[UiElement] = Seq( commonBox(text = "Alerts"), VStack().withChildren( diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Grids.scala b/end-to-end-tests/src/main/scala/tests/chakra/Grids.scala index afe9e87a..0e7eadf7 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Grids.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Grids.scala @@ -1,6 +1,5 @@ package tests.chakra -import org.terminal21.client.ConnectedSession import org.terminal21.client.components.UiElement import org.terminal21.client.components.chakra.{Box, SimpleGrid} import tests.chakra.Common.* diff --git a/end-to-end-tests/src/main/scala/tests/chakra/MediaAndIcons.scala b/end-to-end-tests/src/main/scala/tests/chakra/MediaAndIcons.scala index 21bfb2ac..35dd7cae 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/MediaAndIcons.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/MediaAndIcons.scala @@ -6,7 +6,7 @@ import org.terminal21.client.components.chakra.* import tests.chakra.Common.commonBox object MediaAndIcons: - def components(using session: ConnectedSession): Seq[UiElement] = + def components: Seq[UiElement] = Seq( commonBox(text = "Icons"), HStack().withChildren( diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala b/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala index 803f1c65..2d455053 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala @@ -6,7 +6,7 @@ import org.terminal21.client.components.chakra.* import tests.chakra.Common.commonBox object Overlay: - def components(m: ChakraModel)(using Model[ChakraModel]): Seq[UiElement] = + def components(using Model[ChakraModel]): Seq[UiElement] = val box1 = Box().onModelChange: (b, m) => b.withText(m.box1) Seq( diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Stacks.scala b/end-to-end-tests/src/main/scala/tests/chakra/Stacks.scala index ea1cab0c..4b980f36 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Stacks.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Stacks.scala @@ -1,12 +1,11 @@ package tests.chakra -import org.terminal21.client.ConnectedSession import org.terminal21.client.components.UiElement import org.terminal21.client.components.chakra.{Box, HStack, VStack} import tests.chakra.Common.* object Stacks: - def components(using session: ConnectedSession): Seq[UiElement] = + def components: Seq[UiElement] = Seq( commonBox(text = "VStack"), VStack(spacing = Some("24px"), align = Some("stretch")).withChildren( From 891e231b0ed4393fd6a92f90650982f3d2223824 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 29 Feb 2024 17:06:43 +0000 Subject: [PATCH 229/313] - --- .../main/scala/tests/chakra/MediaAndIcons.scala | 1 - .../scala/org/terminal21/client/Controller.scala | 16 ++++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/chakra/MediaAndIcons.scala b/end-to-end-tests/src/main/scala/tests/chakra/MediaAndIcons.scala index 35dd7cae..fff9fbf3 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/MediaAndIcons.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/MediaAndIcons.scala @@ -1,6 +1,5 @@ package tests.chakra -import org.terminal21.client.ConnectedSession import org.terminal21.client.components.UiElement import org.terminal21.client.components.chakra.* import tests.chakra.Common.commonBox diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 7dbb994c..069df17e 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -38,6 +38,14 @@ class Controller[M]( eventHandlers :+ handler ) +object Controller: + def apply[M](initialModel: Model[M], modelComponents: Seq[UiElement])(using session: ConnectedSession): Controller[M] = + new Controller(session.eventIterator, session.renderChanges, modelComponents, initialModel, Nil) + def apply[M](modelComponents: Seq[UiElement])(using initialModel: Model[M], session: ConnectedSession): Controller[M] = + new Controller(session.eventIterator, session.renderChanges, modelComponents, initialModel, Nil) + def noModel(modelComponents: Seq[UiElement])(using session: ConnectedSession): Controller[Unit] = + new Controller(session.eventIterator, session.renderChanges, modelComponents, Model.Standard.unitModel, Nil) + class RenderedController[M]( eventIteratorFactory: => Iterator[CommandEvent], initialModel: Model[M], @@ -164,14 +172,6 @@ class RenderedController[M]( .takeWhile(!_.shouldTerminate) ) -object Controller: - def apply[M](initialModel: Model[M], modelComponents: Seq[UiElement])(using session: ConnectedSession): Controller[M] = - new Controller(session.eventIterator, session.renderChanges, modelComponents, initialModel, Nil) - def apply[M](modelComponents: Seq[UiElement])(using initialModel: Model[M], session: ConnectedSession): Controller[M] = - new Controller(session.eventIterator, session.renderChanges, modelComponents, initialModel, Nil) - def noModel(modelComponents: Seq[UiElement])(using session: ConnectedSession): Controller[Unit] = - new Controller(session.eventIterator, session.renderChanges, modelComponents, Model.Standard.unitModel, Nil) - sealed trait ControllerEvent[M]: def model: M = handled.model def handled: HandledEvent[M] From e0672fa8ded7daee884d4b2383c44eb514168013 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 29 Feb 2024 17:23:26 +0000 Subject: [PATCH 230/313] - --- .../org/terminal21/client/Controller.scala | 10 +- .../client/components/CachedCalculation.scala | 2 +- .../client/components/StdUiCalculation.scala | 160 +++++++++--------- .../terminal21/client/ControllerTest.scala | 9 +- 4 files changed, 90 insertions(+), 91 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 069df17e..45896f2f 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -132,7 +132,7 @@ class RenderedController[M]( ) ) - private def doRenderChanges(newHandled: HandledEvent[M]): HandledEvent[M] = + private def renderChangesWhenModelChanges(newHandled: HandledEvent[M]): HandledEvent[M] = val changeFunctions = for e <- newHandled.componentsByKey.values @@ -147,7 +147,6 @@ class RenderedController[M]( e.withDataStore(dsEmpty) != ne.withDataStore(dsEmpty) .map(_._2) .toList - renderChanges(changed) newHandled.copy(componentsByKey = newHandled.componentsByKey ++ calcComponentsByKeyMap(changed), renderedChanges = changed) def handledEventsIterator: EventIterator[HandledEvent[M]] = @@ -158,9 +157,10 @@ class RenderedController[M]( .scanLeft(initHandled): case (oldHandled, event) => try - val handled2 = invokeEventHandlers(oldHandled, event) + val handled2 = invokeEventHandlers(oldHandled.copy(renderedChanges = Nil), event) val handled3 = invokeComponentEventHandlers(handled2, event) - val newHandled = if oldHandled.model != handled3.model then doRenderChanges(handled3) else handled3.copy(renderedChanges = Nil) + val newHandled = if oldHandled.model != handled3.model then renderChangesWhenModelChanges(handled3) else handled3 + if newHandled.renderedChanges.nonEmpty then renderChanges(newHandled.renderedChanges) newHandled catch case t: Throwable => @@ -208,3 +208,5 @@ object Model: given unitModel: Model[Unit] = Model(()) given booleanFalseModel: Model[Boolean] = Model(false) given booleanTrueModel: Model[Boolean] = Model(true) + +case class RenderChangesEvent(changes: Seq[UiElement]) extends ClientEvent diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/CachedCalculation.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/CachedCalculation.scala index 98a95d8d..ba1cbe84 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/CachedCalculation.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/CachedCalculation.scala @@ -2,7 +2,7 @@ package org.terminal21.client.components import functions.fibers.{Fiber, FiberExecutor} -abstract class CachedCalculation[OUT](using executor: FiberExecutor) extends Calculation[OUT]: +trait CachedCalculation[OUT](using executor: FiberExecutor) extends Calculation[OUT]: def isCached: Boolean def invalidateCache(): Unit def nonCachedCalculation: OUT diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala index 6557b686..b9d3b08c 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala @@ -1,80 +1,80 @@ -//package org.terminal21.client.components -// -//import functions.fibers.FiberExecutor -//import org.terminal21.client.{ConnectedSession, Model} -//import org.terminal21.client.components.UiElement.HasStyle -//import org.terminal21.client.components.chakra.* -// -//import java.util.concurrent.atomic.{AtomicBoolean, AtomicReference} -// -///** Creates a standard UI for a calculation which may take time. While the calculation runs, the UI is grayed out, including the dataUi component. When the -// * calculation completes, it allows for updating the dataUi component. -// * @tparam OUT -// * the return value of the calculation. -// */ -//trait StdUiCalculation[OUT]( -// name: String, -// dataUi: UiElement with HasStyle -//)(using session: ConnectedSession, executor: FiberExecutor) -// extends Calculation[OUT] -// with UiComponent: -// import Model.Standard.unitModel -// private val running = new AtomicBoolean(false) -// private val currentUi = new AtomicReference(dataUi) -// -// protected def updateUi(dataUi: UiElement & HasStyle) = currentUi.set(dataUi) -// -// lazy val badge = Badge() -// lazy val recalc = Button(text = "Recalculate", size = Some("sm"), leftIcon = Some(RepeatIcon())).onClick: event => -// import event.* -// if running.compareAndSet(false, true) then -// try -// reCalculate() -// finally running.set(false) -// handled -// -// override lazy val rendered: Seq[UiElement] = -// val header = Box( -// bg = "green", -// p = 4, -// children = Seq( -// HStack(children = Seq(Text(text = name), badge, recalc)) -// ) -// ) -// Seq(header, dataUi) -// -// override def onError(t: Throwable): Unit = -// session.fireEvent( -// RenderChangesEvent( -// Seq( -// badge.withText(s"Error: ${t.getMessage}").withColorScheme(Some("red")), -// dataUi, -// recalc.withIsDisabled(None) -// ) -// ) -// ) -// super.onError(t) -// -// override protected def whenResultsNotReady(): Unit = -// session.fireEvent( -// RenderChangesEvent( -// Seq( -// badge.withText("Calculating").withColorScheme(Some("purple")), -// currentUi.get().withStyle(dataUi.style + ("filter" -> "grayscale(100%)")), -// recalc.withIsDisabled(Some(true)) -// ) -// ) -// ) -// super.whenResultsNotReady() -// -// override protected def whenResultsReady(results: OUT): Unit = -// val newDataUi = currentUi.get().withStyle(dataUi.style - "filter") -// session.fireEvent( -// RenderChangesEvent( -// Seq( -// badge.withText("Ready").withColorScheme(None), -// newDataUi, -// recalc.withIsDisabled(Some(false)) -// ) -// ) -// ) +package org.terminal21.client.components + +import functions.fibers.FiberExecutor +import org.terminal21.client.{ConnectedSession, Model, RenderChangesEvent} +import org.terminal21.client.components.UiElement.HasStyle +import org.terminal21.client.components.chakra.* + +import java.util.concurrent.atomic.{AtomicBoolean, AtomicReference} + +/** Creates a standard UI for a calculation which may take time. While the calculation runs, the UI is grayed out, including the dataUi component. When the + * calculation completes, it allows for updating the dataUi component. + * @tparam OUT + * the return value of the calculation. + */ +trait StdUiCalculation[OUT]( + name: String, + dataUi: UiElement with HasStyle +)(using session: ConnectedSession, executor: FiberExecutor) + extends Calculation[OUT] + with UiComponent: + import Model.Standard.unitModel + private val running = new AtomicBoolean(false) + private val currentUi = new AtomicReference(dataUi) + + protected def updateUi(dataUi: UiElement & HasStyle) = currentUi.set(dataUi) + + lazy val badge = Badge() + lazy val recalc = Button(text = "Recalculate", size = Some("sm"), leftIcon = Some(RepeatIcon())).onClick: event => + import event.* + if running.compareAndSet(false, true) then + try + reCalculate() + finally running.set(false) + handled + + override lazy val rendered: Seq[UiElement] = + val header = Box( + bg = "green", + p = 4, + children = Seq( + HStack(children = Seq(Text(text = name), badge, recalc)) + ) + ) + Seq(header, dataUi) + + override def onError(t: Throwable): Unit = + session.fireEvent( + RenderChangesEvent( + Seq( + badge.withText(s"Error: ${t.getMessage}").withColorScheme(Some("red")), + dataUi, + recalc.withIsDisabled(None) + ) + ) + ) + super.onError(t) + + override protected def whenResultsNotReady(): Unit = + session.fireEvent( + RenderChangesEvent( + Seq( + badge.withText("Calculating").withColorScheme(Some("purple")), + currentUi.get().withStyle(dataUi.style + ("filter" -> "grayscale(100%)")), + recalc.withIsDisabled(Some(true)) + ) + ) + ) + super.whenResultsNotReady() + + override protected def whenResultsReady(results: OUT): Unit = + val newDataUi = currentUi.get().withStyle(dataUi.style - "filter") + session.fireEvent( + RenderChangesEvent( + Seq( + badge.withText("Ready").withColorScheme(None), + newDataUi, + recalc.withIsDisabled(Some(false)) + ) + ) + ) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 524de8ad..ca679d55 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -190,13 +190,11 @@ class ControllerTest extends AnyFunSuiteLike: handled.map(_.renderedChanges)(1) should be(expected) test("rendered are cleared"): - given model: Model[Int] = Model(0) - var lastRendered = Seq.empty[UiElement] - def renderer(s: Seq[UiElement]): Unit = lastRendered = s - val but = button.onModelChange: (b, m) => + given model: Model[Int] = Model(0) + val but = button.onModelChange: (b, m) => if m == 1 then b.withText(s"changed $m") else b - val handled = newController(model, Seq(buttonClick, checkBoxChange), Seq(but, checkbox), renderer) + val handled = newController(model, Seq(buttonClick, checkBoxChange), Seq(but, checkbox)) .onEvent: event => val h = event.handled.withModel(event.model + 1) if h.model > 1 then h.terminate else h @@ -204,7 +202,6 @@ class ControllerTest extends AnyFunSuiteLike: .handledEventsIterator .toList - lastRendered should be(Nil) val rendered = handled.map(_.renderedChanges) rendered.head should be(Nil) rendered(1) should be(Seq(but.withText("changed 1"))) From 9f79cc204a4a5e25b388522f255c524241b7baaf Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 29 Feb 2024 17:30:20 +0000 Subject: [PATCH 231/313] - --- .../terminal21/sparklib/calculations/SparkCalculation.scala | 2 +- .../org/terminal21/client/components/StdUiCalculation.scala | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala index f456a6f5..07aa5389 100644 --- a/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala +++ b/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala @@ -51,4 +51,4 @@ abstract class StdUiSparkCalculation[OUT: ReadWriter]( dataUi: UiElement with HasStyle )(using ConnectedSession, Model[_], FiberExecutor, SparkSession) extends SparkCalculation[OUT](name) - with StdUiCalculation[OUT](name, dataUi) + with StdUiCalculation[OUT](key, name, dataUi) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala index b9d3b08c..e52873ed 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala @@ -4,6 +4,7 @@ import functions.fibers.FiberExecutor import org.terminal21.client.{ConnectedSession, Model, RenderChangesEvent} import org.terminal21.client.components.UiElement.HasStyle import org.terminal21.client.components.chakra.* +import org.terminal21.collections.TypedMap import java.util.concurrent.atomic.{AtomicBoolean, AtomicReference} @@ -13,8 +14,10 @@ import java.util.concurrent.atomic.{AtomicBoolean, AtomicReference} * the return value of the calculation. */ trait StdUiCalculation[OUT]( + val key: String, name: String, - dataUi: UiElement with HasStyle + dataUi: UiElement with HasStyle, + val dataStore: TypedMap = TypedMap.empty )(using session: ConnectedSession, executor: FiberExecutor) extends Calculation[OUT] with UiComponent: From 2a9639f6df2dad4b156f98b367416dfaed121bd4 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 29 Feb 2024 20:06:34 +0000 Subject: [PATCH 232/313] - --- .../scala/org/terminal21/client/Controller.scala | 8 ++++++-- .../scala/org/terminal21/client/ControllerTest.scala | 12 ++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 45896f2f..9148185f 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -27,7 +27,11 @@ class Controller[M]( def render()(using session: ConnectedSession): RenderedController[M] = val elements = applyModelTo(modelComponents) session.render(elements) - new RenderedController(eventIteratorFactory, initialModel, elements, renderChanges, eventHandlers) + new RenderedController(eventIteratorFactory, initialModel, elements, renderChanges, eventHandlers :+ renderChangesEventHandler) + + private def renderChangesEventHandler: PartialFunction[ControllerEvent[M], HandledEvent[M]] = + case ControllerClientEvent(h, RenderChangesEvent(changes)) => + h.copy(renderedChanges = h.renderedChanges ++ changes) def onEvent(handler: PartialFunction[ControllerEvent[M], HandledEvent[M]]) = new Controller( @@ -147,7 +151,7 @@ class RenderedController[M]( e.withDataStore(dsEmpty) != ne.withDataStore(dsEmpty) .map(_._2) .toList - newHandled.copy(componentsByKey = newHandled.componentsByKey ++ calcComponentsByKeyMap(changed), renderedChanges = changed) + newHandled.copy(componentsByKey = newHandled.componentsByKey ++ calcComponentsByKeyMap(changed), renderedChanges = newHandled.renderedChanges ++ changed) def handledEventsIterator: EventIterator[HandledEvent[M]] = val initHandled = HandledEvent(initialModel.value, calcComponentsByKeyMap(initialComponents), false, Nil) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index ca679d55..5f7b1492 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -259,3 +259,15 @@ class ControllerTest extends AnyFunSuiteLike: .render()(using connectedSession) verify(connectedSession).render(Seq(b.withText("model 5"))) + + test("RenderChangesEvent renders changes"): + given m: Model[Int] = Model(5) + + val connectedSession = mock[ConnectedSession] + val handledEvents = newController(m, Seq(RenderChangesEvent(Seq(button.withText("changed")))), Seq(button)) + .render() + .handledEventsIterator + .toList + + println(handledEvents.map(_.renderedChanges).mkString("\n")) + handledEvents(1).renderedChanges should be(Seq(button.withText("changed"))) From 7e424d8c8aa37d7f02e8914dddff2280fcf4c3bb Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 29 Feb 2024 20:07:08 +0000 Subject: [PATCH 233/313] - --- .../src/test/scala/org/terminal21/client/ControllerTest.scala | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 5f7b1492..4a81a9a1 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -263,11 +263,9 @@ class ControllerTest extends AnyFunSuiteLike: test("RenderChangesEvent renders changes"): given m: Model[Int] = Model(5) - val connectedSession = mock[ConnectedSession] - val handledEvents = newController(m, Seq(RenderChangesEvent(Seq(button.withText("changed")))), Seq(button)) + val handledEvents = newController(m, Seq(RenderChangesEvent(Seq(button.withText("changed")))), Seq(button)) .render() .handledEventsIterator .toList - println(handledEvents.map(_.renderedChanges).mkString("\n")) handledEvents(1).renderedChanges should be(Seq(button.withText("changed"))) From f7394762e7ed53a029fc26d74577045bd82f1d3f Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 29 Feb 2024 20:13:42 +0000 Subject: [PATCH 234/313] - --- .../sparklib/calculations/SparkCalculation.scala | 6 +++--- .../terminal21/client/components/StdUiCalculation.scala | 8 +++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala index 07aa5389..e3103d32 100644 --- a/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala +++ b/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala @@ -46,9 +46,9 @@ trait SparkCalculation[OUT: ReadWriter](name: String)(using executor: FiberExecu override protected def calculation(): OUT = calculateOnce(nonCachedCalculation) abstract class StdUiSparkCalculation[OUT: ReadWriter]( - val key: String, + override val key: String, name: String, dataUi: UiElement with HasStyle )(using ConnectedSession, Model[_], FiberExecutor, SparkSession) - extends SparkCalculation[OUT](name) - with StdUiCalculation[OUT](key, name, dataUi) + extends StdUiCalculation[OUT](key, name, dataUi) + with SparkCalculation[OUT](name) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala index e52873ed..531e2805 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala @@ -13,7 +13,7 @@ import java.util.concurrent.atomic.{AtomicBoolean, AtomicReference} * @tparam OUT * the return value of the calculation. */ -trait StdUiCalculation[OUT]( +abstract class StdUiCalculation[OUT]( val key: String, name: String, dataUi: UiElement with HasStyle, @@ -70,6 +70,12 @@ trait StdUiCalculation[OUT]( ) super.whenResultsNotReady() + override type This = StdUiCalculation[OUT] + + // probably this class needs redesign + override def withKey(key: String): StdUiCalculation[OUT] = ??? + override def withDataStore(ds: TypedMap): StdUiCalculation[OUT] = ??? + override protected def whenResultsReady(results: OUT): Unit = val newDataUi = currentUi.get().withStyle(dataUi.style - "filter") session.fireEvent( From 89fdaab4cd9cc684c99fc574dc62416e06c82e40 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 29 Feb 2024 20:31:59 +0000 Subject: [PATCH 235/313] - --- example-scripts/bouncing-ball.sc | 38 ++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/example-scripts/bouncing-ball.sc b/example-scripts/bouncing-ball.sc index b6cfef0a..acd9d229 100755 --- a/example-scripts/bouncing-ball.sc +++ b/example-scripts/bouncing-ball.sc @@ -6,32 +6,42 @@ // ------------------------------------------------------------------------------ // always import these -import org.terminal21.client.* +import org.terminal21.client.{*, given} import org.terminal21.client.components.* -import org.terminal21.model.SessionOptions +import org.terminal21.model.* // use the chakra components for menus, forms etc, https://chakra-ui.com/docs/components // The scala case classes : https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala import org.terminal21.client.components.chakra.* -import scala.annotation.tailrec - Sessions .withNewSession("bouncing-ball", "C64 bouncing ball") .connect: session => given ConnectedSession = session - given Model[Unit] = Model.Standard.unitModel + + case class Ball(x: Int, y: Int, dx: Int, dy: Int): + def nextPosition: Ball = + val newDx = if x < 0 || x > 600 then -dx else dx + val newDy = if y < 0 || y > 500 then -dy else dy + Ball(x + newDx, y + newDy, newDx, newDy) + case object Ticker extends ClientEvent + + given Model[Ball] = Model(Ball(50, 50, 8, 8)) println( "Files under ~/.terminal21/web will be served under /web . Please place a ball.png file under ~/.terminal21/web/images on the box where the server runs." ) - val ball = Image(src = "/web/images/ball.png") - session.render(Seq(ball)) + val ball = Image(src = "/web/images/ball.png").onModelChange: (b, m) => + b.withStyle("position" -> "fixed", "left" -> (m.x + "px"), "top" -> (m.y + "px")) - @tailrec def animateBall(x: Int, y: Int, dx: Int, dy: Int): Unit = - session.renderChanges(Seq(ball.withStyle("position" -> "fixed", "left" -> (x + "px"), "top" -> (y + "px")))) - Thread.sleep(1000 / 120) - val newDx = if x < 0 || x > 600 then -dx else dx - val newDy = if y < 0 || y > 500 then -dy else dy - if !session.isClosed then animateBall(x + newDx, y + newDy, newDx, newDy) + fiberExecutor.submit: + while !session.isClosed do + session.fireEvent(Ticker) + Thread.sleep(1000 / 60) - animateBall(50, 50, 8, 8) + Controller(Seq(ball)) + .onEvent: + case ControllerClientEvent(handled, Ticker) => + handled.withModel(handled.model.nextPosition) + .render() + .handledEventsIterator + .lastOption From a02589e0c658b49c0226e9e0116d9958300fa076 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 1 Mar 2024 16:11:52 +0000 Subject: [PATCH 236/313] - --- Readme.md | 1 + build.sbt | 2 +- .../main/scala/tests/ChakraComponents.scala | 2 +- .../src/main/scala/tests/LoginPage.scala | 2 +- .../src/main/scala/tests/StdComponents.scala | 2 +- .../main/scala/tests/chakra/Editables.scala | 4 +- .../src/main/scala/tests/chakra/Forms.scala | 22 +- .../main/scala/tests/chakra/Navigation.scala | 2 +- .../src/main/scala/tests/chakra/Overlay.scala | 8 +- .../org/terminal21/collections/TypedMap.scala | 13 +- .../terminal21/collections/TypedMapTest.scala | 9 + .../org/terminal21/client/Controller.scala | 261 +++++++++++------- .../terminal21/client/ControllerTest.scala | 144 ++++++---- 13 files changed, 283 insertions(+), 189 deletions(-) diff --git a/Readme.md b/Readme.md index d6f57855..663eb464 100644 --- a/Readme.md +++ b/Readme.md @@ -173,6 +173,7 @@ Please use the [discussions](https://github.com/kostaskougios/terminal21-restapi - QuickTabs, QuickFormControl - bug fix for old react state re-rendering on new session - event iterators allows idiomatic handling of events and overhaul of the event handling for easier testing and easier development of larger apps +- MVC ## Version 0.21 diff --git a/build.sbt b/build.sbt index 42f5a94b..9c8ba28f 100644 --- a/build.sbt +++ b/build.sbt @@ -1,7 +1,7 @@ /** This build has different sections for each integration. I.e. an http4s section and a kafka section. These sections are not related to each other, please * examine the section you're interested in. */ -val scala3Version = "3.3.1" +val scala3Version = "3.3.3" ThisBuild / version := "0.30" ThisBuild / organization := "io.github.kostaskougios" diff --git a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala index 10c50de9..3c458df0 100644 --- a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala @@ -17,7 +17,7 @@ import tests.chakra.* // react tests reset the session to clear state val krButton = Button("reset", text = "Reset state").onClick: event => - event.handled.withModel(_.copy(rerun = true)).terminate + event.handled.mapModel(_.copy(rerun = true)).terminate def components(m: ChakraModel): Seq[UiElement] = Overlay.components ++ Forms.components( diff --git a/end-to-end-tests/src/main/scala/tests/LoginPage.scala b/end-to-end-tests/src/main/scala/tests/LoginPage.scala index affdfa8e..eb806f24 100644 --- a/end-to-end-tests/src/main/scala/tests/LoginPage.scala +++ b/end-to-end-tests/src/main/scala/tests/LoginPage.scala @@ -36,7 +36,7 @@ class LoginPage(using session: ConnectedSession): import clickEvent.* // if the email is invalid, we will not terminate. We also will render an error that will be visible for 2 seconds val isValidEmail = model.isValidEmail - handled.withModel(_.copy(submitted = isValidEmail, submittedInvalidEmail = !isValidEmail)) + handled.mapModel(_.copy(submitted = isValidEmail, submittedInvalidEmail = !isValidEmail)) val passwordInput = Input(key = "password", `type` = "password", defaultValue = initialModel.value.pwd) .onChange: changeEvent => diff --git a/end-to-end-tests/src/main/scala/tests/StdComponents.scala b/end-to-end-tests/src/main/scala/tests/StdComponents.scala index 256296dc..70ef75d7 100644 --- a/end-to-end-tests/src/main/scala/tests/StdComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/StdComponents.scala @@ -41,7 +41,7 @@ import org.terminal21.client.components.std.* Cookie(name = "std-components-test-cookie", value = "test-cookie-value"), CookieReader(key = "cookie-reader", name = "std-components-test-cookie").onChange: event => import event.* - handled.withModel(_.copy(cookie = s"Cookie value $newValue")) + handled.mapModel(_.copy(cookie = s"Cookie value $newValue")) , cookieValue ) diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala b/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala index b8d2c728..f40fa158 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala @@ -17,7 +17,7 @@ object Editables: ) .onChange: event => import event.* - handled.withModel(_.copy(editableStatus = s"editable1 newValue = $newValue")) + handled.mapModel(_.copy(editableStatus = s"editable1 newValue = $newValue")) val editable2 = Editable(key = "editable2", defaultValue = "For longer maybe-editable texts\nUse an EditableTextarea\nIt uses a textarea control.") .withChildren( @@ -26,7 +26,7 @@ object Editables: ) .onChange: event => import event.* - handled.withModel(_.copy(editableStatus = s"editable2 newValue = $newValue")) + handled.mapModel(_.copy(editableStatus = s"editable2 newValue = $newValue")) Seq( commonBox(text = "Editables"), diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala b/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala index 365541c9..47ab3e6c 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala @@ -18,12 +18,12 @@ object Forms: val email = Input(key = "email", `type` = "email", defaultValue = m.email) .onChange: event => import event.* - handled.withModel(_.copy(email = newValue, formStatus = s"email input new value = $newValue")) + handled.mapModel(_.copy(email = newValue, formStatus = s"email input new value = $newValue")) val description = Textarea(key = "textarea", placeholder = "Please enter a few things about you", defaultValue = "desc") .onChange: event => import event.* - handled.withModel(_.copy(formStatus = s"description input new value = $newValue")) + handled.mapModel(_.copy(formStatus = s"description input new value = $newValue")) val select1 = Select(key = "male/female", placeholder = "Please choose") .withChildren( @@ -32,7 +32,7 @@ object Forms: ) .onChange: event => import event.* - handled.withModel(_.copy(formStatus = s"select1 input new value = $newValue")) + handled.mapModel(_.copy(formStatus = s"select1 input new value = $newValue")) val select2 = Select(key = "select-first-second", defaultValue = "1", bg = Some("tomato"), color = Some("black"), borderColor = Some("yellow")).withChildren( @@ -44,27 +44,27 @@ object Forms: val dob = Input(key = "dob", `type` = "datetime-local") .onChange: event => import event.* - handled.withModel(_.copy(formStatus = s"dob = $newValue")) + handled.mapModel(_.copy(formStatus = s"dob = $newValue")) val color = Input(key = "color", `type` = "color") .onChange: event => import event.* - handled.withModel(_.copy(formStatus = s"color = $newValue")) + handled.mapModel(_.copy(formStatus = s"color = $newValue")) val checkbox2 = Checkbox(key = "cb2", text = "Check 2", defaultChecked = true) .onChange: event => import event.* - handled.withModel(_.copy(formStatus = s"checkbox2 checked is $newValue")) + handled.mapModel(_.copy(formStatus = s"checkbox2 checked is $newValue")) val checkbox1 = Checkbox(key = "cb1", text = "Check 1") .onChange: event => import event.* - handled.withModel(_.copy(formStatus = s"checkbox1 checked is $newValue")) + handled.mapModel(_.copy(formStatus = s"checkbox1 checked is $newValue")) val switch1 = Switch(key = "sw1", text = "Switch 1") .onChange: event => import event.* - handled.withModel(_.copy(formStatus = s"switch1 checked is $newValue")) + handled.mapModel(_.copy(formStatus = s"switch1 checked is $newValue")) val switch2 = Switch(key = "sw2", text = "Switch 2", defaultChecked = true) val radioGroup = RadioGroup(key = "radio", defaultValue = "2") @@ -77,7 +77,7 @@ object Forms: ) .onChange: event => import event.* - handled.withModel(_.copy(formStatus = s"radioGroup newValue=$newValue")) + handled.mapModel(_.copy(formStatus = s"radioGroup newValue=$newValue")) Seq( commonBox(text = "Forms"), @@ -136,12 +136,12 @@ object Forms: Button(key = "save-button", text = "Save", colorScheme = Some("red")) .onClick: event => import event.* - handled.withModel(_.copy(formStatus = s"Saved clicked")) + handled.mapModel(_.copy(formStatus = s"Saved clicked")) , Button(key = "cancel-button", text = "Cancel") .onClick: event => import event.* - handled.withModel(_.copy(formStatus = s"Cancel clicked")) + handled.mapModel(_.copy(formStatus = s"Cancel clicked")) ), radioGroup, status diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Navigation.scala b/end-to-end-tests/src/main/scala/tests/chakra/Navigation.scala index d772ed75..3684a293 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Navigation.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Navigation.scala @@ -39,7 +39,7 @@ object Navigation: Link(key = "google-link", text = "link-external-google", href = "https://www.google.com/", isExternal = Some(true)) .onClick: event => import event.* - handled.withModel(_.copy(linkStatus = "link-clicked")) + handled.mapModel(_.copy(linkStatus = "link-clicked")) , clickedLink ) diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala b/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala index 2d455053..b5f0f92c 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala @@ -20,20 +20,20 @@ object Overlay: MenuItem(key = "download-menu", text = "Download menu-download") .onClick: event => import event.* - handled.withModel(_.copy(box1 = "'Download' clicked")) + handled.mapModel(_.copy(box1 = "'Download' clicked")) , MenuItem(key = "copy-menu", text = "Copy").onClick: event => import event.* - handled.withModel(_.copy(box1 = "'Copy' clicked")) + handled.mapModel(_.copy(box1 = "'Copy' clicked")) , MenuItem(key = "paste-menu", text = "Paste").onClick: event => import event.* - handled.withModel(_.copy(box1 = "'Paste' clicked")) + handled.mapModel(_.copy(box1 = "'Paste' clicked")) , MenuDivider(), MenuItem(key = "exit-menu", text = "Exit").onClick: event => import event.* - handled.withModel(_.copy(box1 = "'Exit' clicked")) + handled.mapModel(_.copy(box1 = "'Exit' clicked")) ) ), box1 diff --git a/terminal21-server-client-common/src/main/scala/org/terminal21/collections/TypedMap.scala b/terminal21-server-client-common/src/main/scala/org/terminal21/collections/TypedMap.scala index 0de17d70..0dbfac70 100644 --- a/terminal21-server-client-common/src/main/scala/org/terminal21/collections/TypedMap.scala +++ b/terminal21-server-client-common/src/main/scala/org/terminal21/collections/TypedMap.scala @@ -1,10 +1,13 @@ package org.terminal21.collections -class TypedMap(val m: Map[TypedMapKey[_], Any]): +type TMMap = Map[TypedMapKey[_], Any] + +class TypedMap(protected val m: TMMap): def +[A](kv: (TypedMapKey[A], A)): TypedMap = new TypedMap(m + kv) def apply[A](k: TypedMapKey[A]): A = m(k).asInstanceOf[A] def get[A](k: TypedMapKey[A]): Option[A] = m.get(k).asInstanceOf[Option[A]] def getOrElse[A](k: TypedMapKey[A], default: => A) = m.getOrElse(k, default).asInstanceOf[A] + def keys: Iterable[TypedMapKey[_]] = m.keys override def hashCode() = m.hashCode() override def equals(obj: Any) = obj match @@ -15,6 +18,10 @@ class TypedMap(val m: Map[TypedMapKey[_], Any]): override def toString = s"TypedMap(${m.keys.mkString(", ")})" object TypedMap: - def empty = new TypedMap(Map.empty) + def empty = new TypedMap(Map.empty) + def apply(kv: (TypedMapKey[_], Any)*) = + val m = Map(kv*) + new TypedMap(m) -trait TypedMapKey[A] +trait TypedMapKey[A]: + type Of = A diff --git a/terminal21-server-client-common/src/test/scala/org/terminal21/collections/TypedMapTest.scala b/terminal21-server-client-common/src/test/scala/org/terminal21/collections/TypedMapTest.scala index 674592b3..1a961d54 100644 --- a/terminal21-server-client-common/src/test/scala/org/terminal21/collections/TypedMapTest.scala +++ b/terminal21-server-client-common/src/test/scala/org/terminal21/collections/TypedMapTest.scala @@ -12,6 +12,15 @@ class TypedMapTest extends AnyFunSuiteLike: m(IntKey) should be(5) m(StringKey) should be("x") + test("construct"): + val m = TypedMap(IntKey -> 5, StringKey -> "x") + m(IntKey) should be(5) + m(StringKey) should be("x") + + test("keys"): + val m = TypedMap(IntKey -> 5, StringKey -> "x") + m.keys.toSet should be(Set(IntKey, StringKey)) + test("get"): val m = TypedMap.empty + (IntKey -> 5) + (StringKey -> "x") m.get(IntKey) should be(Some(5)) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 9148185f..b434face 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -6,114 +6,127 @@ import org.terminal21.client.components.OnChangeEventHandler.CanHandleOnChangeEv import org.terminal21.client.components.OnClickEventHandler.CanHandleOnClickEvent import org.terminal21.client.components.UiElement.HasChildren import org.terminal21.client.components.{OnChangeBooleanEventHandler, OnChangeEventHandler, OnClickEventHandler, UiElement} -import org.terminal21.collections.{TypedMap, TypedMapKey} +import org.terminal21.collections.{TMMap, TypedMap, TypedMapKey} import org.terminal21.model.{ClientEvent, CommandEvent, OnChange, OnClick} +type EventHandler = PartialFunction[ControllerEvent[_], Handled[_]] +type ComponentsByKey = Map[String, UiElement] class Controller[M]( eventIteratorFactory: => Iterator[CommandEvent], renderChanges: Seq[UiElement] => Unit, modelComponents: Seq[UiElement], - initialModel: Model[M], - eventHandlers: Seq[PartialFunction[ControllerEvent[M], HandledEvent[M]]] + initialModels: Map[Model[Any], Any], + eventHandlers: Seq[EventHandler] ): private def applyModelTo(components: Seq[UiElement]): Seq[UiElement] = - components.map: e => - val ne = if e.hasModelChangeHandler(using initialModel) then e.fireModelChange(using initialModel)(initialModel.value) else e - ne match - case ch: HasChildren => ch.withChildren(applyModelTo(ch.children)*) - case x => x + initialModels + .flatMap: (m, v) => + components.map: e => + val ne = if e.hasModelChangeHandler(using m) then e.fireModelChange(using m)(v) else e + ne match + case ch: HasChildren => ch.withChildren(applyModelTo(ch.children)*) + case x => x + .toList def render()(using session: ConnectedSession): RenderedController[M] = val elements = applyModelTo(modelComponents) session.render(elements) - new RenderedController(eventIteratorFactory, initialModel, elements, renderChanges, eventHandlers :+ renderChangesEventHandler) + new RenderedController(eventIteratorFactory, initialModels, elements, renderChanges, eventHandlers :+ renderChangesEventHandler :+ modelChangeEventHandler) - private def renderChangesEventHandler: PartialFunction[ControllerEvent[M], HandledEvent[M]] = - case ControllerClientEvent(h, RenderChangesEvent(changes)) => + private def renderChangesEventHandler: PartialFunction[ControllerEvent[_], Handled[_]] = + case ControllerClientEvent(h, RenderChangesEvent(changes), _) => h.copy(renderedChanges = h.renderedChanges ++ changes) + private def modelChangeEventHandler: PartialFunction[ControllerEvent[_], Handled[_]] = + case ControllerClientEvent(h, ModelChangeEvent(model, newValue), _) => + h.withModel(model, newValue) - def onEvent(handler: PartialFunction[ControllerEvent[M], HandledEvent[M]]) = + def onEvent(handler: EventHandler) = new Controller( eventIteratorFactory, renderChanges, modelComponents, - initialModel, + initialModels, eventHandlers :+ handler ) object Controller: - def apply[M](initialModel: Model[M], modelComponents: Seq[UiElement])(using session: ConnectedSession): Controller[M] = - new Controller(session.eventIterator, session.renderChanges, modelComponents, initialModel, Nil) - def apply[M](modelComponents: Seq[UiElement])(using initialModel: Model[M], session: ConnectedSession): Controller[M] = - new Controller(session.eventIterator, session.renderChanges, modelComponents, initialModel, Nil) - def noModel(modelComponents: Seq[UiElement])(using session: ConnectedSession): Controller[Unit] = - new Controller(session.eventIterator, session.renderChanges, modelComponents, Model.Standard.unitModel, Nil) +// def apply[M](initialModel: Model[M], modelComponents: Seq[UiElement])(using session: ConnectedSession): Controller[M] = +// new Controller(session.eventIterator, session.renderChanges, modelComponents, initialModel, Nil) + def apply[M](initialValue: M, modelComponents: Seq[UiElement])(using initialModel: Model[M], session: ConnectedSession): Controller[M] = + new Controller(session.eventIterator, session.renderChanges, modelComponents, Map(initialModel.asInstanceOf[Model[Any]] -> initialValue), Nil) + def noModel(modelComponents: Seq[UiElement])(using session: ConnectedSession): Controller[Unit] = + apply((), modelComponents)(using Model.Standard.unitModel, session) class RenderedController[M]( eventIteratorFactory: => Iterator[CommandEvent], - initialModel: Model[M], + initialModels: Map[Model[Any], Any], initialComponents: Seq[UiElement], renderChanges: Seq[UiElement] => Unit, - eventHandlers: Seq[PartialFunction[ControllerEvent[M], HandledEvent[M]]] + eventHandlers: Seq[EventHandler] ): - private val logger = LoggerFactory.getLogger(getClass) - private def clickHandlersMap(h: HandledEvent[M]): Map[String, Seq[OnClickEventHandlerFunction[M]]] = - h.componentsByKey.values + private val logger = LoggerFactory.getLogger(getClass) + + private def invokeEventHandlers[A](initHandled: Handled[A], componentsByKey: ComponentsByKey, event: CommandEvent): Handled[A] = + eventHandlers + .foldLeft(initHandled): (h, f) => + event match + case OnClick(key) => + val e = ControllerClickEvent(componentsByKey(key), h, h.model) + if f.isDefinedAt(e) then f(e).asInstanceOf[Handled[A]] else h + case OnChange(key, value) => + val receivedBy = componentsByKey(key) + val e = receivedBy match + case _: OnChangeEventHandler.CanHandleOnChangeEvent => ControllerChangeEvent(receivedBy, h, value, h.model) + case _: OnChangeBooleanEventHandler.CanHandleOnChangeEvent => ControllerChangeBooleanEvent(receivedBy, h, value.toBoolean, h.model) + if f.isDefinedAt(e) then f(e).asInstanceOf[Handled[A]] else h + case ce: ClientEvent => + val e = ControllerClientEvent(h, ce, h.model) + if f.isDefinedAt(e) then f(e).asInstanceOf[Handled[A]] else h + case x => throw new IllegalStateException(s"Unexpected state $x") + + private def clickHandlersMap[A](allComponents: Seq[UiElement], h: Handled[A]): Map[String, Seq[OnClickEventHandlerFunction[A]]] = + allComponents .collect: - case e: OnClickEventHandler.CanHandleOnClickEvent if e.dataStore.contains(initialModel.ClickKey) => (e.key, e.dataStore(initialModel.ClickKey)) + case e: OnClickEventHandler.CanHandleOnClickEvent if e.dataStore.contains(h.mm.ClickKey) => (e.key, e.dataStore(h.mm.ClickKey)) .toMap - private def changeHandlersMap(h: HandledEvent[M]): Map[String, Seq[OnChangeEventHandlerFunction[M]]] = - h.componentsByKey.values + + private def changeHandlersMap[A](allComponents: Seq[UiElement], h: Handled[A]): Map[String, Seq[OnChangeEventHandlerFunction[A]]] = + allComponents .collect: - case e: OnChangeEventHandler.CanHandleOnChangeEvent if e.dataStore.contains(initialModel.ChangeKey) => (e.key, e.dataStore(initialModel.ChangeKey)) + case e: OnChangeEventHandler.CanHandleOnChangeEvent if e.dataStore.contains(h.mm.ChangeKey) => (e.key, e.dataStore(h.mm.ChangeKey)) .toMap - private def changeBooleanHandlersMap(h: HandledEvent[M]): Map[String, Seq[OnChangeBooleanEventHandlerFunction[M]]] = - h.componentsByKey.values + + private def changeBooleanHandlersMap[A](allComponents: Seq[UiElement], h: Handled[A]): Map[String, Seq[OnChangeBooleanEventHandlerFunction[A]]] = + allComponents .collect: - case e: OnChangeBooleanEventHandler.CanHandleOnChangeEvent if e.dataStore.contains(initialModel.ChangeBooleanKey) => - (e.key, e.dataStore(initialModel.ChangeBooleanKey)) + case e: OnChangeBooleanEventHandler.CanHandleOnChangeEvent if e.dataStore.contains(h.mm.ChangeBooleanKey) => + (e.key, e.dataStore(h.mm.ChangeBooleanKey)) .toMap - private def invokeEventHandlers(initHandled: HandledEvent[M], event: CommandEvent): HandledEvent[M] = - eventHandlers.foldLeft(initHandled): (h, f) => - event match - case OnClick(key) => - val e = ControllerClickEvent(h.componentsByKey(key), h) - if f.isDefinedAt(e) then f(e) else h - case OnChange(key, value) => - val receivedBy = h.componentsByKey(key) - val e = receivedBy match - case _: OnChangeEventHandler.CanHandleOnChangeEvent => ControllerChangeEvent(receivedBy, h, value) - case _: OnChangeBooleanEventHandler.CanHandleOnChangeEvent => ControllerChangeBooleanEvent(receivedBy, h, value.toBoolean) - if f.isDefinedAt(e) then f(e) else h - case ce: ClientEvent => - val e = ControllerClientEvent(h, ce) - if f.isDefinedAt(e) then f(e) else h - case x => throw new IllegalStateException(s"Unexpected state $x") - - private def invokeComponentEventHandlers(h: HandledEvent[M], event: CommandEvent): HandledEvent[M] = - lazy val clickHandlers = clickHandlersMap(h) - lazy val changeHandlers = changeHandlersMap(h) - lazy val changeBooleanHandlers = changeBooleanHandlersMap(h) + private def invokeComponentEventHandlers[A](h: Handled[A], componentsByKey: ComponentsByKey, event: CommandEvent): Handled[A] = + val allComponents = componentsByKey.values.toList + lazy val clickHandlers = clickHandlersMap(allComponents, h) + lazy val changeHandlers = changeHandlersMap(allComponents, h) + lazy val changeBooleanHandlers = changeBooleanHandlersMap(allComponents, h) event match case OnClick(key) if clickHandlers.contains(key) => val handlers = clickHandlers(key) - val receivedBy = h.componentsByKey(key) + val receivedBy = componentsByKey(key) val handled = handlers.foldLeft(h): (handled, handler) => - handler(ControllerClickEvent(receivedBy, handled)) + handler(ControllerClickEvent(receivedBy, handled, handled.model)) handled case OnChange(key, value) if changeHandlers.contains(key) => val handlers = changeHandlers(key) - val receivedBy = h.componentsByKey(key) + val receivedBy = componentsByKey(key) val handled = handlers.foldLeft(h): (handled, handler) => - handler(ControllerChangeEvent(receivedBy, handled, value)) + handler(ControllerChangeEvent(receivedBy, handled, value, handled.model)) handled case OnChange(key, value) if changeBooleanHandlers.contains(key) => val handlers = changeBooleanHandlers(key) - val receivedBy = h.componentsByKey(key) + val receivedBy = componentsByKey(key) val handled = handlers.foldLeft(h): (handled, handler) => - handler(ControllerChangeBooleanEvent(receivedBy, handled, value.toBoolean)) + handler(ControllerChangeBooleanEvent(receivedBy, handled, value.toBoolean, handled.model)) handled case _ => h @@ -136,40 +149,63 @@ class RenderedController[M]( ) ) - private def renderChangesWhenModelChanges(newHandled: HandledEvent[M]): HandledEvent[M] = - val changeFunctions = - for - e <- newHandled.componentsByKey.values - f <- e.dataStore.get(initialModel.OnModelChangeKey) - yield (e, f) - - val dsEmpty = TypedMap.empty - val changed = changeFunctions - .map: (e, f) => - (e, f(e, newHandled.model)) - .filter: (e, ne) => - e.withDataStore(dsEmpty) != ne.withDataStore(dsEmpty) - .map(_._2) - .toList - newHandled.copy(componentsByKey = newHandled.componentsByKey ++ calcComponentsByKeyMap(changed), renderedChanges = newHandled.renderedChanges ++ changed) + private def renderChangesWhenModelChanges[A]( + oldHandled: Handled[A], + newHandled: Handled[A], + componentsByKey: ComponentsByKey + ): (ComponentsByKey, Handled[A]) = + if oldHandled.model == newHandled.model then (componentsByKey, newHandled) + else + val changeFunctions = + for + e <- componentsByKey.values + f <- e.dataStore.get(newHandled.mm.OnModelChangeKey) + yield (e, f) + + val dsEmpty = TypedMap.empty + val changed = changeFunctions + .map: (e, f) => + (e, f(e, newHandled.model)) + .filter: (e, ne) => + e.withDataStore(dsEmpty) != ne.withDataStore(dsEmpty) + .map(_._2) + .toList + ( + componentsByKey ++ calcComponentsByKeyMap(changed), + newHandled.copy(renderedChanges = newHandled.renderedChanges ++ changed) + ) + + private def initialModelsMap: TypedMap = + val m = initialModels.map: (k, v) => + (k.ModelKey, v) + new TypedMap(m.asInstanceOf[TMMap]) def handledEventsIterator: EventIterator[HandledEvent[M]] = - val initHandled = HandledEvent(initialModel.value, calcComponentsByKeyMap(initialComponents), false, Nil) + val initHandledEvent = + HandledEvent[M](initialModels.keys.toSeq, initialModelsMap, calcComponentsByKeyMap(initialComponents), false, Nil) new EventIterator( eventIteratorFactory .takeWhile(!_.isSessionClosed) - .scanLeft(initHandled): - case (oldHandled, event) => + .scanLeft(initHandledEvent): + case (oh, event) => try - val handled2 = invokeEventHandlers(oldHandled.copy(renderedChanges = Nil), event) - val handled3 = invokeComponentEventHandlers(handled2, event) - val newHandled = if oldHandled.model != handled3.model then renderChangesWhenModelChanges(handled3) else handled3 - if newHandled.renderedChanges.nonEmpty then renderChanges(newHandled.renderedChanges) - newHandled + oh.models.foldLeft(oh): + case (oldHandledEvent, model) => + val oldHandled = oldHandledEvent.toHandled(model).copy(renderedChanges = Nil) + val handled2 = invokeEventHandlers(oldHandled, oldHandledEvent.componentsByKey, event) + val handled3 = invokeComponentEventHandlers(handled2, oldHandledEvent.componentsByKey, event) + val (componentsByKey, newHandled) = renderChangesWhenModelChanges(oldHandled, handled3, oldHandledEvent.componentsByKey) + if newHandled.renderedChanges.nonEmpty then renderChanges(newHandled.renderedChanges) + oldHandledEvent.copy( + modelValues = newHandled.models, + componentsByKey = componentsByKey, + shouldTerminate = newHandled.shouldTerminate, + renderedChanges = newHandled.renderedChanges + ) catch case t: Throwable => logger.error("an error occurred while iterating events", t) - oldHandled + oh .flatMap: h => // trick to make sure we take the last state of the model when shouldTerminate=true if h.shouldTerminate then Seq(h.copy(shouldTerminate = false), h) else Seq(h) @@ -177,40 +213,57 @@ class RenderedController[M]( ) sealed trait ControllerEvent[M]: - def model: M = handled.model - def handled: HandledEvent[M] + def model: M + def handled: Handled[M] + +case class ControllerClickEvent[M](clicked: UiElement, handled: Handled[M], model: M) extends ControllerEvent[M] + +case class ControllerChangeEvent[M](changed: UiElement, handled: Handled[M], newValue: String, model: M) extends ControllerEvent[M] + +case class ControllerChangeBooleanEvent[M](changed: UiElement, handled: Handled[M], newValue: Boolean, model: M) extends ControllerEvent[M] +case class ControllerClientEvent[M](handled: Handled[M], event: ClientEvent, model: M) extends ControllerEvent[M] -case class ControllerClickEvent[M](clicked: UiElement, handled: HandledEvent[M]) extends ControllerEvent[M] -case class ControllerChangeEvent[M](changed: UiElement, handled: HandledEvent[M], newValue: String) extends ControllerEvent[M] -case class ControllerChangeBooleanEvent[M](changed: UiElement, handled: HandledEvent[M], newValue: Boolean) extends ControllerEvent[M] -case class ControllerClientEvent[M](handled: HandledEvent[M], event: ClientEvent) extends ControllerEvent[M] +case class Handled[M]( + mm: Model[M], + models: TypedMap, + shouldTerminate: Boolean, + renderedChanges: Seq[UiElement] +): + def model: M = models(mm.ModelKey) + def withModel(m: M): Handled[M] = copy(models = models + (mm.ModelKey -> m)) + def withModel[A](model: Model[A], newValue: A): Handled[M] = copy(models = models + (model.ModelKey -> newValue)) + def mapModel(f: M => M): Handled[M] = withModel(f(model)) + def terminate: Handled[M] = copy(shouldTerminate = true) + def withShouldTerminate(t: Boolean): Handled[M] = copy(shouldTerminate = t) case class HandledEvent[M]( - model: M, - componentsByKey: Map[String, UiElement], + models: Seq[Model[_]], + modelValues: TypedMap, + componentsByKey: ComponentsByKey, shouldTerminate: Boolean, renderedChanges: Seq[UiElement] ): - def terminate: HandledEvent[M] = copy(shouldTerminate = true) - def withShouldTerminate(t: Boolean): HandledEvent[M] = copy(shouldTerminate = t) - def withModel(m: M): HandledEvent[M] = copy(model = m) - def withModel(f: M => M): HandledEvent[M] = copy(model = f(model)) + def model[A](using model: Model[A]): A = modelOf(model) + def modelOf[A](model: Model[A]): A = modelValues(model.ModelKey) + def toHandled[A](model: Model[A]): Handled[A] = Handled[A](model, modelValues, shouldTerminate, renderedChanges) -type OnClickEventHandlerFunction[M] = ControllerClickEvent[M] => HandledEvent[M] -type OnChangeEventHandlerFunction[M] = ControllerChangeEvent[M] => HandledEvent[M] -type OnChangeBooleanEventHandlerFunction[M] = ControllerChangeBooleanEvent[M] => HandledEvent[M] +type OnClickEventHandlerFunction[M] = ControllerClickEvent[M] => Handled[M] +type OnChangeEventHandlerFunction[M] = ControllerChangeEvent[M] => Handled[M] +type OnChangeBooleanEventHandlerFunction[M] = ControllerChangeBooleanEvent[M] => Handled[M] -case class Model[M](value: M): +class Model[M]: type OnModelChangeFunction = (UiElement, M) => UiElement + object ModelKey extends TypedMapKey[M] object OnModelChangeKey extends TypedMapKey[OnModelChangeFunction] object ClickKey extends TypedMapKey[Seq[OnClickEventHandlerFunction[M]]] object ChangeKey extends TypedMapKey[Seq[OnChangeEventHandlerFunction[M]]] object ChangeBooleanKey extends TypedMapKey[Seq[OnChangeBooleanEventHandlerFunction[M]]] object Model: + def apply[M]: Model[M] = new Model[M] object Standard: - given unitModel: Model[Unit] = Model(()) - given booleanFalseModel: Model[Boolean] = Model(false) - given booleanTrueModel: Model[Boolean] = Model(true) + given unitModel: Model[Unit] = Model[Unit] + given booleanModel: Model[Boolean] = Model[Boolean] -case class RenderChangesEvent(changes: Seq[UiElement]) extends ClientEvent +case class RenderChangesEvent(changes: Seq[UiElement]) extends ClientEvent +case class ModelChangeEvent[M](model: Model[M], newValue: M) extends ClientEvent diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 4a81a9a1..c146731c 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -22,8 +22,12 @@ class ControllerTest extends AnyFunSuiteLike: val checkBoxChange = OnChange(checkbox.key, "true") given ConnectedSession = ConnectedSessionMock.newConnectedSessionMock + val intModel = Model[Int] + val stringModel = Model[String] + def newController[M]( initialModel: Model[M], + initialValue: M, events: => Seq[CommandEvent], modelComponents: Seq[UiElement], renderChanges: Seq[UiElement] => Unit = _ => () @@ -32,38 +36,39 @@ class ControllerTest extends AnyFunSuiteLike: val it = seList.iterator events.foreach(e => seList.add(e)) seList.add(CommandEvent.sessionClosed) - new Controller(it, renderChanges, modelComponents, initialModel, Nil) + new Controller(it, renderChanges, modelComponents, Map(initialModel.asInstanceOf[Model[Any]] -> initialValue), Nil) test("will throw an exception if there is a duplicate key"): an[IllegalArgumentException] should be thrownBy - newController(Model(0), Seq(buttonClick), Seq(button, button)).render().handledEventsIterator + newController(Model[Int], 0, Seq(buttonClick), Seq(button, button)).render().handledEventsIterator test("onEvent is called"): - val model = Model(0) - newController(model, Seq(buttonClick), Seq(button)) - .onEvent: event => - if event.model > 1 then event.handled.terminate else event.handled.withModel(event.model + 1) + given Model[Int] = intModel + newController(intModel, 0, Seq(buttonClick), Seq(button)) + .onEvent: + case ControllerClickEvent[Int @unchecked](_, handled, model) => + if model > 1 then handled.terminate else handled.withModel(model + 1) .render() .handledEventsIterator .map(_.model) .toList should be(List(0, 1)) test("onEvent is called for change"): - val model = Model(0) - newController(model, Seq(inputChange), Seq(input)) - .onEvent: event => - import event.* - if event.model > 1 then handled.terminate else handled.withModel(event.model + 1) + given Model[Int] = intModel + newController(intModel, 0, Seq(inputChange), Seq(input)) + .onEvent: + case ControllerChangeEvent[Int @unchecked](_, handled, newValue, model) => + if model > 1 then handled.terminate else handled.withModel(model + 1) .render() .handledEventsIterator .map(_.model) .toList should be(List(0, 1)) test("onEvent not matched for change"): - val model = Model(0) - newController(model, Seq(inputChange), Seq(input)) + given Model[Int] = intModel + newController(intModel, 0, Seq(inputChange), Seq(input)) .onEvent: - case event: ControllerClickEvent[_] => + case event: ControllerClickEvent[Int @unchecked] => import event.* handled.withModel(5) .render() @@ -72,21 +77,22 @@ class ControllerTest extends AnyFunSuiteLike: .toList should be(List(0, 0)) test("onEvent is called for change/boolean"): - val model = Model(0) - newController(model, Seq(checkBoxChange), Seq(checkbox)) - .onEvent: event => - import event.* - if event.model > 1 then handled.terminate else handled.withModel(event.model + 1) + given Model[Int] = intModel + newController(intModel, 0, Seq(checkBoxChange), Seq(checkbox)) + .onEvent: + case event: ControllerChangeBooleanEvent[Int @unchecked] => + import event.* + if event.model > 1 then handled.terminate else handled.withModel(event.model + 1) .render() .handledEventsIterator .map(_.model) .toList should be(List(0, 1)) test("onEvent not matches for change/boolean"): - val model = Model(0) - newController(model, Seq(checkBoxChange), Seq(checkbox)) + given Model[Int] = intModel + newController(intModel, 0, Seq(checkBoxChange), Seq(checkbox)) .onEvent: - case event: ControllerClickEvent[_] => + case event: ControllerClickEvent[Int @unchecked] => import event.* handled.withModel(5) .render() @@ -97,11 +103,10 @@ class ControllerTest extends AnyFunSuiteLike: case class TestClientEvent(i: Int) extends ClientEvent test("onEvent is called for ClientEvent"): - val model = Model(0) - newController(model, Seq(TestClientEvent(5)), Seq(button)) + given Model[Int] = intModel + newController(intModel, 0, Seq(TestClientEvent(5)), Seq(button)) .onEvent: - case ControllerClientEvent(handled, event: TestClientEvent) => - import event.* + case ControllerClientEvent[Int @unchecked](handled, event: TestClientEvent, _) => handled.withModel(event.i).terminate .render() .handledEventsIterator @@ -109,10 +114,10 @@ class ControllerTest extends AnyFunSuiteLike: .toList should be(List(0, 5)) test("onEvent when no partial function matches ClientEvent"): - val model = Model(0) - newController(model, Seq(TestClientEvent(5)), Seq(button)) + given Model[Int] = intModel + newController(intModel, 0, Seq(TestClientEvent(5)), Seq(button)) .onEvent: - case ControllerClickEvent(`checkbox`, handled) => + case ControllerClickEvent[Int @unchecked](`checkbox`, handled, _) => handled.withModel(5).terminate .render() .handledEventsIterator @@ -120,9 +125,10 @@ class ControllerTest extends AnyFunSuiteLike: .toList should be(List(0, 0)) test("onClick is called"): - given model: Model[Int] = Model(0) + given Model[Int] = intModel newController( - model, + intModel, + 0, Seq(buttonClick), Seq( button.onClick: event => @@ -134,9 +140,10 @@ class ControllerTest extends AnyFunSuiteLike: .toList should be(List(0, 100)) test("onChange is called"): - given model: Model[Int] = Model(0) + given Model[Int] = intModel newController( - model, + intModel, + 0, Seq(inputChange), Seq( input.onChange: event => @@ -148,9 +155,10 @@ class ControllerTest extends AnyFunSuiteLike: .toList should be(List(0, 100)) test("onChange/boolean is called"): - given model: Model[Int] = Model(0) + given Model[Int] = intModel newController( - model, + intModel, + 0, Seq(checkBoxChange), Seq( checkbox.onChange: event => @@ -162,25 +170,27 @@ class ControllerTest extends AnyFunSuiteLike: .toList should be(List(0, 100)) test("terminate is obeyed and latest model state is iterated"): - val model = Model(0) - newController(model, Seq(buttonClick, buttonClick, buttonClick), Seq(button)) - .onEvent: event => - if event.model > 1 then event.handled.terminate.withModel(100) else event.handled.withModel(event.model + 1) + given Model[Int] = intModel + newController(intModel, 0, Seq(buttonClick, buttonClick, buttonClick), Seq(button)) + .onEvent: + case event: ControllerEvent[Int @unchecked] => + if event.model > 1 then event.handled.terminate.withModel(100) else event.handled.withModel(event.model + 1) .render() .handledEventsIterator .map(_.model) .toList should be(List(0, 1, 2, 100)) test("changes are rendered"): - given model: Model[Int] = Model(0) + given Model[Int] = intModel var rendered = Seq.empty[UiElement] def renderer(s: Seq[UiElement]): Unit = rendered = s val but = button.onModelChange: (b, m) => b.withText(s"changed $m") - val handled = newController(model, Seq(buttonClick), Seq(but), renderer) - .onEvent: event => - event.handled.withModel(event.model + 1).terminate + val handled = newController(intModel, 0, Seq(buttonClick), Seq(but), renderer) + .onEvent: + case event: ControllerEvent[Int @unchecked] => + event.handled.withModel(event.model + 1).terminate .render() .handledEventsIterator .toList @@ -190,14 +200,15 @@ class ControllerTest extends AnyFunSuiteLike: handled.map(_.renderedChanges)(1) should be(expected) test("rendered are cleared"): - given model: Model[Int] = Model(0) - val but = button.onModelChange: (b, m) => + given Model[Int] = intModel + val but = button.onModelChange: (b, m) => if m == 1 then b.withText(s"changed $m") else b - val handled = newController(model, Seq(buttonClick, checkBoxChange), Seq(but, checkbox)) - .onEvent: event => - val h = event.handled.withModel(event.model + 1) - if h.model > 1 then h.terminate else h + val handled = newController(intModel, 0, Seq(buttonClick, checkBoxChange), Seq(but, checkbox)) + .onEvent: + case event: ControllerEvent[Int @unchecked] => + val h = event.handled.withModel(event.model + 1) + if h.model > 1 then h.terminate else h .render() .handledEventsIterator .toList @@ -208,8 +219,8 @@ class ControllerTest extends AnyFunSuiteLike: rendered(2) should be(Nil) test("components handle events"): - given m: Model[Int] = Model(0) - val table = QuickTable().withRows( + given Model[Int] = intModel + val table = QuickTable().withRows( Seq( Seq( button.onClick: event => @@ -218,7 +229,7 @@ class ControllerTest extends AnyFunSuiteLike: ) ) ) - val handledEvents = newController(m, Seq(buttonClick), Seq(table)) + val handledEvents = newController(intModel, 0, Seq(buttonClick), Seq(table)) .render() .handledEventsIterator .toList @@ -226,9 +237,9 @@ class ControllerTest extends AnyFunSuiteLike: handledEvents.map(_.model) should be(List(0, 1)) test("components receive onModelChange"): - given m: Model[Int] = Model(0) - val called = new AtomicBoolean(false) - val table = QuickTable() + given Model[Int] = intModel + val called = new AtomicBoolean(false) + val table = QuickTable() .withRows( Seq( Seq( @@ -241,7 +252,7 @@ class ControllerTest extends AnyFunSuiteLike: .onModelChange: (table, _) => called.set(true) table - newController(m, Seq(buttonClick), Seq(table)) + newController(intModel, 0, Seq(buttonClick), Seq(table)) .render() .handledEventsIterator .lastOption @@ -249,23 +260,36 @@ class ControllerTest extends AnyFunSuiteLike: called.get() should be(true) test("applies initial model before rendering"): - given m: Model[Int] = Model(5) + given Model[Int] = intModel val b = button.onModelChange: (b, m) => b.withText(s"model $m") val connectedSession = mock[ConnectedSession] - newController(m, Nil, Seq(b)) + newController(intModel, 5, Nil, Seq(b)) .render()(using connectedSession) verify(connectedSession).render(Seq(b.withText("model 5"))) test("RenderChangesEvent renders changes"): - given m: Model[Int] = Model(5) + given Model[Int] = intModel - val handledEvents = newController(m, Seq(RenderChangesEvent(Seq(button.withText("changed")))), Seq(button)) + val handledEvents = newController(intModel, 5, Seq(RenderChangesEvent(Seq(button.withText("changed")))), Seq(button)) .render() .handledEventsIterator .toList handledEvents(1).renderedChanges should be(Seq(button.withText("changed"))) + + test("ModelChangeEvent"): + + val handledEvents = newController(stringModel, "v", Seq(ModelChangeEvent(intModel, 6)), Nil).render().handledEventsIterator.toList + handledEvents(1).modelOf(intModel) should be(6) + + test("onModelChange for different model"): + val b1 = button.onModelChange(using intModel): (b, m) => + b.withText(s"changed $m") + + val handledEvents = newController(stringModel, "v", Seq(ModelChangeEvent(intModel, 6)), Seq(b1)).render().handledEventsIterator.toList + + handledEvents(1).renderedChanges should be(Seq(b1.withText("changed 6"))) From d234d296e284aea5d01efacd3280d34b75557992 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 1 Mar 2024 16:30:10 +0000 Subject: [PATCH 237/313] - --- .../scala/org/terminal21/client/Controller.scala | 12 ++++++++---- .../scala/org/terminal21/client/ControllerTest.scala | 4 ++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index b434face..a45f99de 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -9,6 +9,8 @@ import org.terminal21.client.components.{OnChangeBooleanEventHandler, OnChangeEv import org.terminal21.collections.{TMMap, TypedMap, TypedMapKey} import org.terminal21.model.{ClientEvent, CommandEvent, OnChange, OnClick} +import scala.reflect.{ClassTag, classTag} + type EventHandler = PartialFunction[ControllerEvent[_], Handled[_]] type ComponentsByKey = Map[String, UiElement] class Controller[M]( @@ -251,19 +253,21 @@ type OnClickEventHandlerFunction[M] = ControllerClickEvent[M] => Handled type OnChangeEventHandlerFunction[M] = ControllerChangeEvent[M] => Handled[M] type OnChangeBooleanEventHandlerFunction[M] = ControllerChangeBooleanEvent[M] => Handled[M] -class Model[M]: +class Model[M](name: String): type OnModelChangeFunction = (UiElement, M) => UiElement object ModelKey extends TypedMapKey[M] object OnModelChangeKey extends TypedMapKey[OnModelChangeFunction] object ClickKey extends TypedMapKey[Seq[OnClickEventHandlerFunction[M]]] object ChangeKey extends TypedMapKey[Seq[OnChangeEventHandlerFunction[M]]] object ChangeBooleanKey extends TypedMapKey[Seq[OnChangeBooleanEventHandlerFunction[M]]] + override def toString = s"Model($name)" object Model: - def apply[M]: Model[M] = new Model[M] + def apply[M: ClassTag]: Model[M] = new Model[M](classTag[M].runtimeClass.getName) + def apply[M](name: String): Model[M] = new Model[M](name) object Standard: - given unitModel: Model[Unit] = Model[Unit] - given booleanModel: Model[Boolean] = Model[Boolean] + given unitModel: Model[Unit] = Model[Unit]("unit") + given booleanModel: Model[Boolean] = Model[Boolean]("boolean") case class RenderChangesEvent(changes: Seq[UiElement]) extends ClientEvent case class ModelChangeEvent[M](model: Model[M], newValue: M) extends ClientEvent diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index c146731c..e4e8735f 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -22,8 +22,8 @@ class ControllerTest extends AnyFunSuiteLike: val checkBoxChange = OnChange(checkbox.key, "true") given ConnectedSession = ConnectedSessionMock.newConnectedSessionMock - val intModel = Model[Int] - val stringModel = Model[String] + val intModel = Model[Int]("int-model") + val stringModel = Model[String]("string-model") def newController[M]( initialModel: Model[M], From 87a7ece11efc929d34edbaf7a853bd8f8828bac9 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 1 Mar 2024 16:34:48 +0000 Subject: [PATCH 238/313] - --- .../org/terminal21/client/Controller.scala | 17 +++++++++-------- .../org/terminal21/client/ControllerTest.scala | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index a45f99de..8f8222b8 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -13,7 +13,8 @@ import scala.reflect.{ClassTag, classTag} type EventHandler = PartialFunction[ControllerEvent[_], Handled[_]] type ComponentsByKey = Map[String, UiElement] -class Controller[M]( + +class Controller( eventIteratorFactory: => Iterator[CommandEvent], renderChanges: Seq[UiElement] => Unit, modelComponents: Seq[UiElement], @@ -31,7 +32,7 @@ class Controller[M]( case x => x .toList - def render()(using session: ConnectedSession): RenderedController[M] = + def render()(using session: ConnectedSession): RenderedController = val elements = applyModelTo(modelComponents) session.render(elements) new RenderedController(eventIteratorFactory, initialModels, elements, renderChanges, eventHandlers :+ renderChangesEventHandler :+ modelChangeEventHandler) @@ -55,12 +56,12 @@ class Controller[M]( object Controller: // def apply[M](initialModel: Model[M], modelComponents: Seq[UiElement])(using session: ConnectedSession): Controller[M] = // new Controller(session.eventIterator, session.renderChanges, modelComponents, initialModel, Nil) - def apply[M](initialValue: M, modelComponents: Seq[UiElement])(using initialModel: Model[M], session: ConnectedSession): Controller[M] = + def apply[M](initialValue: M, modelComponents: Seq[UiElement])(using initialModel: Model[M], session: ConnectedSession): Controller = new Controller(session.eventIterator, session.renderChanges, modelComponents, Map(initialModel.asInstanceOf[Model[Any]] -> initialValue), Nil) - def noModel(modelComponents: Seq[UiElement])(using session: ConnectedSession): Controller[Unit] = + def noModel(modelComponents: Seq[UiElement])(using session: ConnectedSession): Controller = apply((), modelComponents)(using Model.Standard.unitModel, session) -class RenderedController[M]( +class RenderedController( eventIteratorFactory: => Iterator[CommandEvent], initialModels: Map[Model[Any], Any], initialComponents: Seq[UiElement], @@ -182,9 +183,9 @@ class RenderedController[M]( (k.ModelKey, v) new TypedMap(m.asInstanceOf[TMMap]) - def handledEventsIterator: EventIterator[HandledEvent[M]] = + def handledEventsIterator: EventIterator[HandledEvent] = val initHandledEvent = - HandledEvent[M](initialModels.keys.toSeq, initialModelsMap, calcComponentsByKeyMap(initialComponents), false, Nil) + HandledEvent(initialModels.keys.toSeq, initialModelsMap, calcComponentsByKeyMap(initialComponents), false, Nil) new EventIterator( eventIteratorFactory .takeWhile(!_.isSessionClosed) @@ -238,7 +239,7 @@ case class Handled[M]( def terminate: Handled[M] = copy(shouldTerminate = true) def withShouldTerminate(t: Boolean): Handled[M] = copy(shouldTerminate = t) -case class HandledEvent[M]( +case class HandledEvent( models: Seq[Model[_]], modelValues: TypedMap, componentsByKey: ComponentsByKey, diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index e4e8735f..6ac4e064 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -31,7 +31,7 @@ class ControllerTest extends AnyFunSuiteLike: events: => Seq[CommandEvent], modelComponents: Seq[UiElement], renderChanges: Seq[UiElement] => Unit = _ => () - ): Controller[M] = + ): Controller = val seList = SEList[CommandEvent]() val it = seList.iterator events.foreach(e => seList.add(e)) From c622f1481b0210512b86f8c91bff794b69fd055e Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 1 Mar 2024 16:38:01 +0000 Subject: [PATCH 239/313] - --- .../terminal21/client/ControllerTest.scala | 48 ++++++++++--------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 6ac4e064..cfeeeada 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -22,8 +22,9 @@ class ControllerTest extends AnyFunSuiteLike: val checkBoxChange = OnChange(checkbox.key, "true") given ConnectedSession = ConnectedSessionMock.newConnectedSessionMock - val intModel = Model[Int]("int-model") - val stringModel = Model[String]("string-model") + object Givens: + given intModel: Model[Int] = Model[Int]("int-model") + given stringModel: Model[String] = Model[String]("string-model") def newController[M]( initialModel: Model[M], @@ -43,7 +44,7 @@ class ControllerTest extends AnyFunSuiteLike: newController(Model[Int], 0, Seq(buttonClick), Seq(button, button)).render().handledEventsIterator test("onEvent is called"): - given Model[Int] = intModel + import Givens.intModel newController(intModel, 0, Seq(buttonClick), Seq(button)) .onEvent: case ControllerClickEvent[Int @unchecked](_, handled, model) => @@ -54,7 +55,7 @@ class ControllerTest extends AnyFunSuiteLike: .toList should be(List(0, 1)) test("onEvent is called for change"): - given Model[Int] = intModel + import Givens.intModel newController(intModel, 0, Seq(inputChange), Seq(input)) .onEvent: case ControllerChangeEvent[Int @unchecked](_, handled, newValue, model) => @@ -65,7 +66,7 @@ class ControllerTest extends AnyFunSuiteLike: .toList should be(List(0, 1)) test("onEvent not matched for change"): - given Model[Int] = intModel + import Givens.intModel newController(intModel, 0, Seq(inputChange), Seq(input)) .onEvent: case event: ControllerClickEvent[Int @unchecked] => @@ -77,7 +78,7 @@ class ControllerTest extends AnyFunSuiteLike: .toList should be(List(0, 0)) test("onEvent is called for change/boolean"): - given Model[Int] = intModel + import Givens.intModel newController(intModel, 0, Seq(checkBoxChange), Seq(checkbox)) .onEvent: case event: ControllerChangeBooleanEvent[Int @unchecked] => @@ -89,7 +90,7 @@ class ControllerTest extends AnyFunSuiteLike: .toList should be(List(0, 1)) test("onEvent not matches for change/boolean"): - given Model[Int] = intModel + import Givens.intModel newController(intModel, 0, Seq(checkBoxChange), Seq(checkbox)) .onEvent: case event: ControllerClickEvent[Int @unchecked] => @@ -103,7 +104,7 @@ class ControllerTest extends AnyFunSuiteLike: case class TestClientEvent(i: Int) extends ClientEvent test("onEvent is called for ClientEvent"): - given Model[Int] = intModel + import Givens.intModel newController(intModel, 0, Seq(TestClientEvent(5)), Seq(button)) .onEvent: case ControllerClientEvent[Int @unchecked](handled, event: TestClientEvent, _) => @@ -114,7 +115,7 @@ class ControllerTest extends AnyFunSuiteLike: .toList should be(List(0, 5)) test("onEvent when no partial function matches ClientEvent"): - given Model[Int] = intModel + import Givens.intModel newController(intModel, 0, Seq(TestClientEvent(5)), Seq(button)) .onEvent: case ControllerClickEvent[Int @unchecked](`checkbox`, handled, _) => @@ -125,7 +126,7 @@ class ControllerTest extends AnyFunSuiteLike: .toList should be(List(0, 0)) test("onClick is called"): - given Model[Int] = intModel + import Givens.intModel newController( intModel, 0, @@ -140,7 +141,7 @@ class ControllerTest extends AnyFunSuiteLike: .toList should be(List(0, 100)) test("onChange is called"): - given Model[Int] = intModel + import Givens.intModel newController( intModel, 0, @@ -155,7 +156,7 @@ class ControllerTest extends AnyFunSuiteLike: .toList should be(List(0, 100)) test("onChange/boolean is called"): - given Model[Int] = intModel + import Givens.intModel newController( intModel, 0, @@ -170,7 +171,7 @@ class ControllerTest extends AnyFunSuiteLike: .toList should be(List(0, 100)) test("terminate is obeyed and latest model state is iterated"): - given Model[Int] = intModel + import Givens.intModel newController(intModel, 0, Seq(buttonClick, buttonClick, buttonClick), Seq(button)) .onEvent: case event: ControllerEvent[Int @unchecked] => @@ -181,7 +182,7 @@ class ControllerTest extends AnyFunSuiteLike: .toList should be(List(0, 1, 2, 100)) test("changes are rendered"): - given Model[Int] = intModel + import Givens.intModel var rendered = Seq.empty[UiElement] def renderer(s: Seq[UiElement]): Unit = rendered = s val but = button.onModelChange: (b, m) => @@ -200,8 +201,8 @@ class ControllerTest extends AnyFunSuiteLike: handled.map(_.renderedChanges)(1) should be(expected) test("rendered are cleared"): - given Model[Int] = intModel - val but = button.onModelChange: (b, m) => + import Givens.intModel + val but = button.onModelChange: (b, m) => if m == 1 then b.withText(s"changed $m") else b val handled = newController(intModel, 0, Seq(buttonClick, checkBoxChange), Seq(but, checkbox)) @@ -219,7 +220,7 @@ class ControllerTest extends AnyFunSuiteLike: rendered(2) should be(Nil) test("components handle events"): - given Model[Int] = intModel + import Givens.intModel val table = QuickTable().withRows( Seq( Seq( @@ -237,9 +238,9 @@ class ControllerTest extends AnyFunSuiteLike: handledEvents.map(_.model) should be(List(0, 1)) test("components receive onModelChange"): - given Model[Int] = intModel - val called = new AtomicBoolean(false) - val table = QuickTable() + import Givens.intModel + val called = new AtomicBoolean(false) + val table = QuickTable() .withRows( Seq( Seq( @@ -260,7 +261,7 @@ class ControllerTest extends AnyFunSuiteLike: called.get() should be(true) test("applies initial model before rendering"): - given Model[Int] = intModel + import Givens.intModel val b = button.onModelChange: (b, m) => b.withText(s"model $m") @@ -272,7 +273,7 @@ class ControllerTest extends AnyFunSuiteLike: verify(connectedSession).render(Seq(b.withText("model 5"))) test("RenderChangesEvent renders changes"): - given Model[Int] = intModel + import Givens.intModel val handledEvents = newController(intModel, 5, Seq(RenderChangesEvent(Seq(button.withText("changed")))), Seq(button)) .render() @@ -282,11 +283,12 @@ class ControllerTest extends AnyFunSuiteLike: handledEvents(1).renderedChanges should be(Seq(button.withText("changed"))) test("ModelChangeEvent"): - + import Givens.given val handledEvents = newController(stringModel, "v", Seq(ModelChangeEvent(intModel, 6)), Nil).render().handledEventsIterator.toList handledEvents(1).modelOf(intModel) should be(6) test("onModelChange for different model"): + import Givens.given val b1 = button.onModelChange(using intModel): (b, m) => b.withText(s"changed $m") From 4f45be117f1946cad2c83daa3026dc0f737d10b2 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 1 Mar 2024 16:40:27 +0000 Subject: [PATCH 240/313] - --- .../org/terminal21/client/Controller.scala | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 8f8222b8..b59cd319 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -18,12 +18,12 @@ class Controller( eventIteratorFactory: => Iterator[CommandEvent], renderChanges: Seq[UiElement] => Unit, modelComponents: Seq[UiElement], - initialModels: Map[Model[Any], Any], + initialModelValues: Map[Model[Any], Any], eventHandlers: Seq[EventHandler] ): private def applyModelTo(components: Seq[UiElement]): Seq[UiElement] = - initialModels + initialModelValues .flatMap: (m, v) => components.map: e => val ne = if e.hasModelChangeHandler(using m) then e.fireModelChange(using m)(v) else e @@ -35,7 +35,13 @@ class Controller( def render()(using session: ConnectedSession): RenderedController = val elements = applyModelTo(modelComponents) session.render(elements) - new RenderedController(eventIteratorFactory, initialModels, elements, renderChanges, eventHandlers :+ renderChangesEventHandler :+ modelChangeEventHandler) + new RenderedController( + eventIteratorFactory, + initialModelValues, + elements, + renderChanges, + eventHandlers :+ renderChangesEventHandler :+ modelChangeEventHandler + ) private def renderChangesEventHandler: PartialFunction[ControllerEvent[_], Handled[_]] = case ControllerClientEvent(h, RenderChangesEvent(changes), _) => @@ -49,7 +55,7 @@ class Controller( eventIteratorFactory, renderChanges, modelComponents, - initialModels, + initialModelValues, eventHandlers :+ handler ) @@ -63,7 +69,7 @@ object Controller: class RenderedController( eventIteratorFactory: => Iterator[CommandEvent], - initialModels: Map[Model[Any], Any], + initialModelValues: Map[Model[Any], Any], initialComponents: Seq[UiElement], renderChanges: Seq[UiElement] => Unit, eventHandlers: Seq[EventHandler] @@ -179,13 +185,13 @@ class RenderedController( ) private def initialModelsMap: TypedMap = - val m = initialModels.map: (k, v) => + val m = initialModelValues.map: (k, v) => (k.ModelKey, v) new TypedMap(m.asInstanceOf[TMMap]) def handledEventsIterator: EventIterator[HandledEvent] = val initHandledEvent = - HandledEvent(initialModels.keys.toSeq, initialModelsMap, calcComponentsByKeyMap(initialComponents), false, Nil) + HandledEvent(initialModelValues.keys.toSeq, initialModelsMap, calcComponentsByKeyMap(initialComponents), false, Nil) new EventIterator( eventIteratorFactory .takeWhile(!_.isSessionClosed) From d94e821812b8792887b00c4a682daf19907422f2 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 1 Mar 2024 17:22:06 +0000 Subject: [PATCH 241/313] - --- .../org/terminal21/collections/TypedMap.scala | 6 ++- .../org/terminal21/client/Controller.scala | 38 ++++++++++--------- .../client/components/UiElement.scala | 12 ++++-- .../terminal21/client/ControllerTest.scala | 11 +++++- 4 files changed, 43 insertions(+), 24 deletions(-) diff --git a/terminal21-server-client-common/src/main/scala/org/terminal21/collections/TypedMap.scala b/terminal21-server-client-common/src/main/scala/org/terminal21/collections/TypedMap.scala index 0dbfac70..f60bbb66 100644 --- a/terminal21-server-client-common/src/main/scala/org/terminal21/collections/TypedMap.scala +++ b/terminal21-server-client-common/src/main/scala/org/terminal21/collections/TypedMap.scala @@ -1,5 +1,7 @@ package org.terminal21.collections +import scala.reflect.{ClassTag, classTag} + type TMMap = Map[TypedMapKey[_], Any] class TypedMap(protected val m: TMMap): @@ -23,5 +25,7 @@ object TypedMap: val m = Map(kv*) new TypedMap(m) -trait TypedMapKey[A]: +trait TypedMapKey[A: ClassTag]: type Of = A + + override def toString = s"${getClass.getSimpleName}[${classTag[A].runtimeClass.getSimpleName}]" diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index b59cd319..9b7c575c 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -40,15 +40,12 @@ class Controller( initialModelValues, elements, renderChanges, - eventHandlers :+ renderChangesEventHandler :+ modelChangeEventHandler + eventHandlers :+ renderChangesEventHandler ) private def renderChangesEventHandler: PartialFunction[ControllerEvent[_], Handled[_]] = case ControllerClientEvent(h, RenderChangesEvent(changes), _) => h.copy(renderedChanges = h.renderedChanges ++ changes) - private def modelChangeEventHandler: PartialFunction[ControllerEvent[_], Handled[_]] = - case ControllerClientEvent(h, ModelChangeEvent(model, newValue), _) => - h.withModel(model, newValue) def onEvent(handler: EventHandler) = new Controller( @@ -118,6 +115,7 @@ class RenderedController( lazy val clickHandlers = clickHandlersMap(allComponents, h) lazy val changeHandlers = changeHandlersMap(allComponents, h) lazy val changeBooleanHandlers = changeBooleanHandlersMap(allComponents, h) + println(event.toString + "/" + h.mm) event match case OnClick(key) if clickHandlers.contains(key) => val handlers = clickHandlers(key) @@ -137,6 +135,8 @@ class RenderedController( val handled = handlers.foldLeft(h): (handled, handler) => handler(ControllerChangeBooleanEvent(receivedBy, handled, value.toBoolean, handled.model)) handled + case ModelChangeEvent(model, newValue) if model == h.mm => + h.withModel(model, newValue) case _ => h private def checkForDuplicatesAndThrow(components: Seq[UiElement]): Unit = @@ -189,16 +189,20 @@ class RenderedController( (k.ModelKey, v) new TypedMap(m.asInstanceOf[TMMap]) + private def availableModels(componentsByKey: ComponentsByKey): Seq[Model[_]] = + (initialModelValues.keys.toList ++ componentsByKey.values.flatMap(_.handledModels).toList).distinct + def handledEventsIterator: EventIterator[HandledEvent] = - val initHandledEvent = - HandledEvent(initialModelValues.keys.toSeq, initialModelsMap, calcComponentsByKeyMap(initialComponents), false, Nil) + val initCompByKeyMap = calcComponentsByKeyMap(initialComponents) + val initAvailableModels = availableModels(initCompByKeyMap) + val initHandledEvent = HandledEvent(initAvailableModels, initialModelsMap, initCompByKeyMap, false, Nil) new EventIterator( eventIteratorFactory .takeWhile(!_.isSessionClosed) .scanLeft(initHandledEvent): - case (oh, event) => + case (ohEvent, event) => try - oh.models.foldLeft(oh): + ohEvent.models.foldLeft(ohEvent): case (oldHandledEvent, model) => val oldHandled = oldHandledEvent.toHandled(model).copy(renderedChanges = Nil) val handled2 = invokeEventHandlers(oldHandled, oldHandledEvent.componentsByKey, event) @@ -206,7 +210,7 @@ class RenderedController( val (componentsByKey, newHandled) = renderChangesWhenModelChanges(oldHandled, handled3, oldHandledEvent.componentsByKey) if newHandled.renderedChanges.nonEmpty then renderChanges(newHandled.renderedChanges) oldHandledEvent.copy( - modelValues = newHandled.models, + modelValues = newHandled.modelValues, componentsByKey = componentsByKey, shouldTerminate = newHandled.shouldTerminate, renderedChanges = newHandled.renderedChanges @@ -214,7 +218,7 @@ class RenderedController( catch case t: Throwable => logger.error("an error occurred while iterating events", t) - oh + ohEvent .flatMap: h => // trick to make sure we take the last state of the model when shouldTerminate=true if h.shouldTerminate then Seq(h.copy(shouldTerminate = false), h) else Seq(h) @@ -234,13 +238,13 @@ case class ControllerClientEvent[M](handled: Handled[M], event: ClientEvent, mod case class Handled[M]( mm: Model[M], - models: TypedMap, + modelValues: TypedMap, shouldTerminate: Boolean, renderedChanges: Seq[UiElement] ): - def model: M = models(mm.ModelKey) - def withModel(m: M): Handled[M] = copy(models = models + (mm.ModelKey -> m)) - def withModel[A](model: Model[A], newValue: A): Handled[M] = copy(models = models + (model.ModelKey -> newValue)) + def model: M = modelValues(mm.ModelKey) + def withModel(m: M): Handled[M] = copy(modelValues = modelValues + (mm.ModelKey -> m)) + def withModel[A](model: Model[A], newValue: A): Handled[M] = copy(modelValues = modelValues + (model.ModelKey -> newValue)) def mapModel(f: M => M): Handled[M] = withModel(f(model)) def terminate: Handled[M] = copy(shouldTerminate = true) def withShouldTerminate(t: Boolean): Handled[M] = copy(shouldTerminate = t) @@ -260,7 +264,7 @@ type OnClickEventHandlerFunction[M] = ControllerClickEvent[M] => Handled type OnChangeEventHandlerFunction[M] = ControllerChangeEvent[M] => Handled[M] type OnChangeBooleanEventHandlerFunction[M] = ControllerChangeBooleanEvent[M] => Handled[M] -class Model[M](name: String): +class Model[M: ClassTag](name: String): type OnModelChangeFunction = (UiElement, M) => UiElement object ModelKey extends TypedMapKey[M] object OnModelChangeKey extends TypedMapKey[OnModelChangeFunction] @@ -270,8 +274,8 @@ class Model[M](name: String): override def toString = s"Model($name)" object Model: - def apply[M: ClassTag]: Model[M] = new Model[M](classTag[M].runtimeClass.getName) - def apply[M](name: String): Model[M] = new Model[M](name) + def apply[M: ClassTag]: Model[M] = new Model[M](classTag[M].runtimeClass.getName) + def apply[M: ClassTag](name: String): Model[M] = new Model[M](name) object Standard: given unitModel: Model[Unit] = Model[Unit]("unit") given booleanModel: Model[Boolean] = Model[Boolean]("boolean") diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala index 7c946b62..b09f31f2 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala @@ -1,7 +1,7 @@ package org.terminal21.client.components import org.terminal21.client.Model -import org.terminal21.client.components.UiElement.HasChildren +import org.terminal21.client.components.UiElement.{HasChildren, UiElementModelsKey} import org.terminal21.client.components.chakra.Box import org.terminal21.collections.{TypedMap, TypedMapKey} @@ -19,10 +19,12 @@ abstract class UiElement extends AnyElement: /** This handler will be called whenever the model changes. It will also be called with the initial model before the first render() */ def onModelChange[M](using model: Model[M])(f: (This, M) => This): This = - store(model.OnModelChangeKey, f.asInstanceOf[model.OnModelChangeFunction]) - def hasModelChangeHandler[M](using model: Model[M]): Boolean = dataStore.contains(model.OnModelChangeKey) - def fireModelChange[M](using model: Model[M])(m: M) = + store(UiElementModelsKey, handledModels :+ model).store(model.OnModelChangeKey, f.asInstanceOf[model.OnModelChangeFunction]).asInstanceOf[This] + + def hasModelChangeHandler[M](using model: Model[M]): Boolean = dataStore.contains(model.OnModelChangeKey) + def fireModelChange[M](using model: Model[M])(m: M) = dataStore(model.OnModelChangeKey).apply(this, m) + def handledModels: Seq[Model[_]] = dataStore.get(UiElementModelsKey).toSeq.flatten /** @return * this element along all it's children flattened @@ -38,6 +40,8 @@ abstract class UiElement extends AnyElement: def toSimpleString: String = s"${getClass.getSimpleName}($key)" object UiElement: + object UiElementModelsKey extends TypedMapKey[Seq[Model[_]]] + trait HasChildren: this: UiElement => def children: Seq[UiElement] diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index cfeeeada..1cc8176c 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -32,12 +32,19 @@ class ControllerTest extends AnyFunSuiteLike: events: => Seq[CommandEvent], modelComponents: Seq[UiElement], renderChanges: Seq[UiElement] => Unit = _ => () + ): Controller = newControllerWith(Seq(initialModel -> initialValue), events, modelComponents, renderChanges) + + def newControllerWith( + modelValues: Seq[(Model[_], _)], + events: => Seq[CommandEvent], + modelComponents: Seq[UiElement], + renderChanges: Seq[UiElement] => Unit = _ => () ): Controller = val seList = SEList[CommandEvent]() val it = seList.iterator events.foreach(e => seList.add(e)) seList.add(CommandEvent.sessionClosed) - new Controller(it, renderChanges, modelComponents, Map(initialModel.asInstanceOf[Model[Any]] -> initialValue), Nil) + new Controller(it, renderChanges, modelComponents, modelValues.toMap.asInstanceOf[Map[Model[Any], Any]], Nil) test("will throw an exception if there is a duplicate key"): an[IllegalArgumentException] should be thrownBy @@ -284,7 +291,7 @@ class ControllerTest extends AnyFunSuiteLike: test("ModelChangeEvent"): import Givens.given - val handledEvents = newController(stringModel, "v", Seq(ModelChangeEvent(intModel, 6)), Nil).render().handledEventsIterator.toList + val handledEvents = newControllerWith(Seq(stringModel -> "v",intModel -> 5), Seq(ModelChangeEvent(intModel, 6)), Nil).render().handledEventsIterator.toList handledEvents(1).modelOf(intModel) should be(6) test("onModelChange for different model"): From e0e97559f466636cdeda0da8800ab01ca228f9b3 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 1 Mar 2024 17:25:46 +0000 Subject: [PATCH 242/313] - --- .../org/terminal21/client/Controller.scala | 26 +++++++++---------- .../terminal21/client/ControllerTest.scala | 16 ++++++------ 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 9b7c575c..8b5182eb 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -44,7 +44,7 @@ class Controller( ) private def renderChangesEventHandler: PartialFunction[ControllerEvent[_], Handled[_]] = - case ControllerClientEvent(h, RenderChangesEvent(changes), _) => + case ControllerClientEvent(h, RenderChangesEvent(changes)) => h.copy(renderedChanges = h.renderedChanges ++ changes) def onEvent(handler: EventHandler) = @@ -78,16 +78,16 @@ class RenderedController( .foldLeft(initHandled): (h, f) => event match case OnClick(key) => - val e = ControllerClickEvent(componentsByKey(key), h, h.model) + val e = ControllerClickEvent(componentsByKey(key), h) if f.isDefinedAt(e) then f(e).asInstanceOf[Handled[A]] else h case OnChange(key, value) => val receivedBy = componentsByKey(key) val e = receivedBy match - case _: OnChangeEventHandler.CanHandleOnChangeEvent => ControllerChangeEvent(receivedBy, h, value, h.model) - case _: OnChangeBooleanEventHandler.CanHandleOnChangeEvent => ControllerChangeBooleanEvent(receivedBy, h, value.toBoolean, h.model) + case _: OnChangeEventHandler.CanHandleOnChangeEvent => ControllerChangeEvent(receivedBy, h, value) + case _: OnChangeBooleanEventHandler.CanHandleOnChangeEvent => ControllerChangeBooleanEvent(receivedBy, h, value.toBoolean) if f.isDefinedAt(e) then f(e).asInstanceOf[Handled[A]] else h case ce: ClientEvent => - val e = ControllerClientEvent(h, ce, h.model) + val e = ControllerClientEvent(h, ce) if f.isDefinedAt(e) then f(e).asInstanceOf[Handled[A]] else h case x => throw new IllegalStateException(s"Unexpected state $x") @@ -121,19 +121,19 @@ class RenderedController( val handlers = clickHandlers(key) val receivedBy = componentsByKey(key) val handled = handlers.foldLeft(h): (handled, handler) => - handler(ControllerClickEvent(receivedBy, handled, handled.model)) + handler(ControllerClickEvent(receivedBy, handled)) handled case OnChange(key, value) if changeHandlers.contains(key) => val handlers = changeHandlers(key) val receivedBy = componentsByKey(key) val handled = handlers.foldLeft(h): (handled, handler) => - handler(ControllerChangeEvent(receivedBy, handled, value, handled.model)) + handler(ControllerChangeEvent(receivedBy, handled, value)) handled case OnChange(key, value) if changeBooleanHandlers.contains(key) => val handlers = changeBooleanHandlers(key) val receivedBy = componentsByKey(key) val handled = handlers.foldLeft(h): (handled, handler) => - handler(ControllerChangeBooleanEvent(receivedBy, handled, value.toBoolean, handled.model)) + handler(ControllerChangeBooleanEvent(receivedBy, handled, value.toBoolean)) handled case ModelChangeEvent(model, newValue) if model == h.mm => h.withModel(model, newValue) @@ -226,15 +226,15 @@ class RenderedController( ) sealed trait ControllerEvent[M]: - def model: M + def model: M = handled.model def handled: Handled[M] -case class ControllerClickEvent[M](clicked: UiElement, handled: Handled[M], model: M) extends ControllerEvent[M] +case class ControllerClickEvent[M](clicked: UiElement, handled: Handled[M]) extends ControllerEvent[M] -case class ControllerChangeEvent[M](changed: UiElement, handled: Handled[M], newValue: String, model: M) extends ControllerEvent[M] +case class ControllerChangeEvent[M](changed: UiElement, handled: Handled[M], newValue: String) extends ControllerEvent[M] -case class ControllerChangeBooleanEvent[M](changed: UiElement, handled: Handled[M], newValue: Boolean, model: M) extends ControllerEvent[M] -case class ControllerClientEvent[M](handled: Handled[M], event: ClientEvent, model: M) extends ControllerEvent[M] +case class ControllerChangeBooleanEvent[M](changed: UiElement, handled: Handled[M], newValue: Boolean) extends ControllerEvent[M] +case class ControllerClientEvent[M](handled: Handled[M], event: ClientEvent) extends ControllerEvent[M] case class Handled[M]( mm: Model[M], diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 1cc8176c..2cbe1af4 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -54,8 +54,8 @@ class ControllerTest extends AnyFunSuiteLike: import Givens.intModel newController(intModel, 0, Seq(buttonClick), Seq(button)) .onEvent: - case ControllerClickEvent[Int @unchecked](_, handled, model) => - if model > 1 then handled.terminate else handled.withModel(model + 1) + case ControllerClickEvent[Int @unchecked](_, handled) => + if handled.model > 1 then handled.terminate else handled.withModel(handled.model + 1) .render() .handledEventsIterator .map(_.model) @@ -65,8 +65,8 @@ class ControllerTest extends AnyFunSuiteLike: import Givens.intModel newController(intModel, 0, Seq(inputChange), Seq(input)) .onEvent: - case ControllerChangeEvent[Int @unchecked](_, handled, newValue, model) => - if model > 1 then handled.terminate else handled.withModel(model + 1) + case ControllerChangeEvent[Int @unchecked](_, handled, newValue) => + if handled.model > 1 then handled.terminate else handled.withModel(handled.model + 1) .render() .handledEventsIterator .map(_.model) @@ -114,7 +114,7 @@ class ControllerTest extends AnyFunSuiteLike: import Givens.intModel newController(intModel, 0, Seq(TestClientEvent(5)), Seq(button)) .onEvent: - case ControllerClientEvent[Int @unchecked](handled, event: TestClientEvent, _) => + case ControllerClientEvent[Int @unchecked](handled, event: TestClientEvent) => handled.withModel(event.i).terminate .render() .handledEventsIterator @@ -125,7 +125,7 @@ class ControllerTest extends AnyFunSuiteLike: import Givens.intModel newController(intModel, 0, Seq(TestClientEvent(5)), Seq(button)) .onEvent: - case ControllerClickEvent[Int @unchecked](`checkbox`, handled, _) => + case ControllerClickEvent[Int @unchecked](`checkbox`, handled) => handled.withModel(5).terminate .render() .handledEventsIterator @@ -182,7 +182,7 @@ class ControllerTest extends AnyFunSuiteLike: newController(intModel, 0, Seq(buttonClick, buttonClick, buttonClick), Seq(button)) .onEvent: case event: ControllerEvent[Int @unchecked] => - if event.model > 1 then event.handled.terminate.withModel(100) else event.handled.withModel(event.model + 1) + if event.handled.model > 1 then event.handled.terminate.withModel(100) else event.handled.withModel(event.handled.model + 1) .render() .handledEventsIterator .map(_.model) @@ -291,7 +291,7 @@ class ControllerTest extends AnyFunSuiteLike: test("ModelChangeEvent"): import Givens.given - val handledEvents = newControllerWith(Seq(stringModel -> "v",intModel -> 5), Seq(ModelChangeEvent(intModel, 6)), Nil).render().handledEventsIterator.toList + val handledEvents = newControllerWith(Seq(stringModel -> "v", intModel -> 5), Seq(ModelChangeEvent(intModel, 6)), Nil).render().handledEventsIterator.toList handledEvents(1).modelOf(intModel) should be(6) test("onModelChange for different model"): From cafebaf09dc32744cc5561927b4ea40888e4ca8d Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 1 Mar 2024 17:27:48 +0000 Subject: [PATCH 243/313] - --- .../src/main/scala/org/terminal21/client/Controller.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 8b5182eb..f121f1d9 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -163,7 +163,7 @@ class RenderedController( newHandled: Handled[A], componentsByKey: ComponentsByKey ): (ComponentsByKey, Handled[A]) = - if oldHandled.model == newHandled.model then (componentsByKey, newHandled) + if oldHandled.modelOption == newHandled.modelOption then (componentsByKey, newHandled) else val changeFunctions = for @@ -243,6 +243,7 @@ case class Handled[M]( renderedChanges: Seq[UiElement] ): def model: M = modelValues(mm.ModelKey) + def modelOption: Option[M] = modelValues.get(mm.ModelKey) def withModel(m: M): Handled[M] = copy(modelValues = modelValues + (mm.ModelKey -> m)) def withModel[A](model: Model[A], newValue: A): Handled[M] = copy(modelValues = modelValues + (model.ModelKey -> newValue)) def mapModel(f: M => M): Handled[M] = withModel(f(model)) From fa9c8a203d1c1369bf4d303e9ba7e314250a82dc Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 1 Mar 2024 17:36:37 +0000 Subject: [PATCH 244/313] - --- .../src/main/scala/tests/LoginPage.scala | 4 ++-- .../src/main/scala/tests/StdComponents.scala | 4 ++-- .../src/main/scala/tests/chakra/Editables.scala | 2 +- .../src/main/scala/tests/chakra/Forms.scala | 4 ++-- .../src/main/scala/tests/chakra/Navigation.scala | 4 ++-- .../src/main/scala/tests/chakra/Overlay.scala | 2 +- .../serverapp/bundled/ServerStatusApp.scala | 4 ++-- .../serverapp/bundled/ServerStatusPageTest.scala | 2 +- .../scala/org/terminal21/client/Controller.scala | 14 +++++++------- .../terminal21/client/components/UiElement.scala | 13 ++++++------- .../org/terminal21/client/ControllerTest.scala | 10 +++++----- 11 files changed, 31 insertions(+), 32 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/LoginPage.scala b/end-to-end-tests/src/main/scala/tests/LoginPage.scala index eb806f24..3ed1500e 100644 --- a/end-to-end-tests/src/main/scala/tests/LoginPage.scala +++ b/end-to-end-tests/src/main/scala/tests/LoginPage.scala @@ -64,7 +64,7 @@ class LoginPage(using session: ConnectedSession): .withInputGroup( InputLeftAddon().withChildren(EmailIcon()), emailInput, - InputRightAddon().onModelChange: (i, m) => + InputRightAddon().onModelChangeRender: (i, m) => i.withChildren(if m.isValidEmail then okIcon else notOkIcon) ), QuickFormControl() @@ -75,7 +75,7 @@ class LoginPage(using session: ConnectedSession): passwordInput ), submitButton, - errorsBox.onModelChange: (eb, m) => + errorsBox.onModelChangeRender: (eb, m) => if m.submittedInvalidEmail then eb.withChildren(errorMsgInvalidEmail) else errorsBox ) diff --git a/end-to-end-tests/src/main/scala/tests/StdComponents.scala b/end-to-end-tests/src/main/scala/tests/StdComponents.scala index 70ef75d7..7c6040c2 100644 --- a/end-to-end-tests/src/main/scala/tests/StdComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/StdComponents.scala @@ -14,9 +14,9 @@ import org.terminal21.client.components.std.* given model: Model[Form] = Model(Form("This will reflect what you type in the input", "This will display the value of the cookie")) def components = - val output = Paragraph().onModelChange: (p, m) => + val output = Paragraph().onModelChangeRender: (p, m) => p.withText(m.output) - val cookieValue = Paragraph().onModelChange: (p, m) => + val cookieValue = Paragraph().onModelChangeRender: (p, m) => p.withText(m.cookie) val input = Input(key = "name", defaultValue = "Please enter your name").onChange: event => import event.* diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala b/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala index f40fa158..eecbcd3a 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala @@ -7,7 +7,7 @@ import tests.chakra.Common.* object Editables: def components(using Model[ChakraModel]): Seq[UiElement] = - val status = Box().onModelChange: (b, m) => + val status = Box().onModelChangeRender: (b, m) => b.withText(m.editableStatus) val editable1 = Editable(key = "editable1", defaultValue = "Please type here") diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala b/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala index 47ab3e6c..1298a78a 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala @@ -7,12 +7,12 @@ import tests.chakra.Common.* object Forms: def components(m: ChakraModel)(using Model[ChakraModel]): Seq[UiElement] = - val status = Box().onModelChange: (b, m) => + val status = Box().onModelChangeRender: (b, m) => b.withText(m.formStatus) val okIcon = CheckCircleIcon(color = Some("green")) val notOkIcon = WarningTwoIcon(color = Some("red")) - val emailRightAddOn = InputRightAddon().onModelChange: (i, m) => + val emailRightAddOn = InputRightAddon().onModelChangeRender: (i, m) => i.withChildren(if m.email.contains("@") then okIcon else notOkIcon) val email = Input(key = "email", `type` = "email", defaultValue = m.email) diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Navigation.scala b/end-to-end-tests/src/main/scala/tests/chakra/Navigation.scala index 3684a293..a63583b4 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Navigation.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Navigation.scala @@ -8,11 +8,11 @@ import tests.chakra.Common.commonBox object Navigation: def components(using Model[ChakraModel]): Seq[UiElement] = - val clickedBreadcrumb = Paragraph().onModelChange: (p, m) => + val clickedBreadcrumb = Paragraph().onModelChangeRender: (p, m) => p.withText(m.breadcrumbStatus) def breadcrumbClicked(m: ChakraModel, t: String) = m.copy(breadcrumbStatus = s"breadcrumb-click: $t") - val clickedLink = Paragraph().onModelChange: (p, m) => + val clickedLink = Paragraph().onModelChangeRender: (p, m) => p.withText(m.linkStatus) Seq( diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala b/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala index b5f0f92c..883f36d1 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala @@ -7,7 +7,7 @@ import tests.chakra.Common.commonBox object Overlay: def components(using Model[ChakraModel]): Seq[UiElement] = - val box1 = Box().onModelChange: (b, m) => + val box1 = Box().onModelChangeRender: (b, m) => b.withText(m.box1) Seq( commonBox(text = "Menus box0001"), diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 75d6d282..4588fbd9 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -58,7 +58,7 @@ class ServerStatusPage( event.handled def jvmTable: UiElement = - jvmTableE.onModelChange: (table, m) => + jvmTableE.onModelChangeRender: (table, m) => val runtime = m.runtime table.withRows( Seq( @@ -76,7 +76,7 @@ class ServerStatusPage( ).withHeaders("Id", "Name", "Is Open", "Actions") def sessionsTable: UiElement = - sessionsTableE.onModelChange: (table, m) => + sessionsTableE.onModelChangeRender: (table, m) => val sessions = m.sessions table.withRows( sessions.map: session => diff --git a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala index cf59eaf0..1ab71f71 100644 --- a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala +++ b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala @@ -47,7 +47,7 @@ class ServerStatusPageTest extends AnyFunSuiteLike: val table = page.sessionsTable val m = page.initModel.copy(sessions = Seq(session(isOpen = false))) table - .fireModelChange(m) + .fireModelChangeRender(m) .flat .collectFirst: case i: NotAllowedIcon => i diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index f121f1d9..b3fc4675 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -26,7 +26,7 @@ class Controller( initialModelValues .flatMap: (m, v) => components.map: e => - val ne = if e.hasModelChangeHandler(using m) then e.fireModelChange(using m)(v) else e + val ne = if e.hasModelChangeRenderHandler(using m) then e.fireModelChangeRender(using m)(v) else e ne match case ch: HasChildren => ch.withChildren(applyModelTo(ch.children)*) case x => x @@ -168,7 +168,7 @@ class RenderedController( val changeFunctions = for e <- componentsByKey.values - f <- e.dataStore.get(newHandled.mm.OnModelChangeKey) + f <- e.dataStore.get(newHandled.mm.OnModelChangeRenderKey) yield (e, f) val dsEmpty = TypedMap.empty @@ -267,11 +267,11 @@ type OnChangeBooleanEventHandlerFunction[M] = ControllerChangeBooleanEvent[M] => class Model[M: ClassTag](name: String): type OnModelChangeFunction = (UiElement, M) => UiElement - object ModelKey extends TypedMapKey[M] - object OnModelChangeKey extends TypedMapKey[OnModelChangeFunction] - object ClickKey extends TypedMapKey[Seq[OnClickEventHandlerFunction[M]]] - object ChangeKey extends TypedMapKey[Seq[OnChangeEventHandlerFunction[M]]] - object ChangeBooleanKey extends TypedMapKey[Seq[OnChangeBooleanEventHandlerFunction[M]]] + object ModelKey extends TypedMapKey[M] + object OnModelChangeRenderKey extends TypedMapKey[OnModelChangeFunction] + object ClickKey extends TypedMapKey[Seq[OnClickEventHandlerFunction[M]]] + object ChangeKey extends TypedMapKey[Seq[OnChangeEventHandlerFunction[M]]] + object ChangeBooleanKey extends TypedMapKey[Seq[OnChangeBooleanEventHandlerFunction[M]]] override def toString = s"Model($name)" object Model: diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala index b09f31f2..07802113 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala @@ -18,13 +18,12 @@ abstract class UiElement extends AnyElement: /** This handler will be called whenever the model changes. It will also be called with the initial model before the first render() */ - def onModelChange[M](using model: Model[M])(f: (This, M) => This): This = - store(UiElementModelsKey, handledModels :+ model).store(model.OnModelChangeKey, f.asInstanceOf[model.OnModelChangeFunction]).asInstanceOf[This] - - def hasModelChangeHandler[M](using model: Model[M]): Boolean = dataStore.contains(model.OnModelChangeKey) - def fireModelChange[M](using model: Model[M])(m: M) = - dataStore(model.OnModelChangeKey).apply(this, m) - def handledModels: Seq[Model[_]] = dataStore.get(UiElementModelsKey).toSeq.flatten + def onModelChangeRender[M](using model: Model[M])(f: (This, M) => This): This = + store(UiElementModelsKey, handledModels :+ model).store(model.OnModelChangeRenderKey, f.asInstanceOf[model.OnModelChangeFunction]).asInstanceOf[This] + def hasModelChangeRenderHandler[M](using model: Model[M]): Boolean = dataStore.contains(model.OnModelChangeRenderKey) + def fireModelChangeRender[M](using model: Model[M])(m: M) = + dataStore(model.OnModelChangeRenderKey).apply(this, m) + def handledModels: Seq[Model[_]] = dataStore.get(UiElementModelsKey).toSeq.flatten /** @return * this element along all it's children flattened diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 2cbe1af4..b91aec99 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -192,7 +192,7 @@ class ControllerTest extends AnyFunSuiteLike: import Givens.intModel var rendered = Seq.empty[UiElement] def renderer(s: Seq[UiElement]): Unit = rendered = s - val but = button.onModelChange: (b, m) => + val but = button.onModelChangeRender: (b, m) => b.withText(s"changed $m") val handled = newController(intModel, 0, Seq(buttonClick), Seq(but), renderer) @@ -209,7 +209,7 @@ class ControllerTest extends AnyFunSuiteLike: test("rendered are cleared"): import Givens.intModel - val but = button.onModelChange: (b, m) => + val but = button.onModelChangeRender: (b, m) => if m == 1 then b.withText(s"changed $m") else b val handled = newController(intModel, 0, Seq(buttonClick, checkBoxChange), Seq(but, checkbox)) @@ -257,7 +257,7 @@ class ControllerTest extends AnyFunSuiteLike: ) ) ) - .onModelChange: (table, _) => + .onModelChangeRender: (table, _) => called.set(true) table newController(intModel, 0, Seq(buttonClick), Seq(table)) @@ -270,7 +270,7 @@ class ControllerTest extends AnyFunSuiteLike: test("applies initial model before rendering"): import Givens.intModel - val b = button.onModelChange: (b, m) => + val b = button.onModelChangeRender: (b, m) => b.withText(s"model $m") val connectedSession = mock[ConnectedSession] @@ -296,7 +296,7 @@ class ControllerTest extends AnyFunSuiteLike: test("onModelChange for different model"): import Givens.given - val b1 = button.onModelChange(using intModel): (b, m) => + val b1 = button.onModelChangeRender(using intModel): (b, m) => b.withText(s"changed $m") val handledEvents = newController(stringModel, "v", Seq(ModelChangeEvent(intModel, 6)), Seq(b1)).render().handledEventsIterator.toList From 0b57f3ecaf3afd9e83b17977211f7c1819448a23 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 1 Mar 2024 17:41:07 +0000 Subject: [PATCH 245/313] - --- .../main/scala/org/terminal21/client/Controller.scala | 8 ++++++++ .../scala/org/terminal21/client/ControllerTest.scala | 11 ++--------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index b3fc4675..613ac278 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -21,6 +21,14 @@ class Controller( initialModelValues: Map[Model[Any], Any], eventHandlers: Seq[EventHandler] ): + def model[M](using model: Model[M])(value: M): Controller = + new Controller( + eventIteratorFactory, + renderChanges, + modelComponents, + initialModelValues + (model.asInstanceOf[Model[Any]] -> value), + eventHandlers + ) private def applyModelTo(components: Seq[UiElement]): Seq[UiElement] = initialModelValues diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index b91aec99..085296d0 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -32,19 +32,12 @@ class ControllerTest extends AnyFunSuiteLike: events: => Seq[CommandEvent], modelComponents: Seq[UiElement], renderChanges: Seq[UiElement] => Unit = _ => () - ): Controller = newControllerWith(Seq(initialModel -> initialValue), events, modelComponents, renderChanges) - - def newControllerWith( - modelValues: Seq[(Model[_], _)], - events: => Seq[CommandEvent], - modelComponents: Seq[UiElement], - renderChanges: Seq[UiElement] => Unit = _ => () ): Controller = val seList = SEList[CommandEvent]() val it = seList.iterator events.foreach(e => seList.add(e)) seList.add(CommandEvent.sessionClosed) - new Controller(it, renderChanges, modelComponents, modelValues.toMap.asInstanceOf[Map[Model[Any], Any]], Nil) + new Controller(it, renderChanges, modelComponents, Map.empty, Nil).model(using initialModel)(initialValue) test("will throw an exception if there is a duplicate key"): an[IllegalArgumentException] should be thrownBy @@ -291,7 +284,7 @@ class ControllerTest extends AnyFunSuiteLike: test("ModelChangeEvent"): import Givens.given - val handledEvents = newControllerWith(Seq(stringModel -> "v", intModel -> 5), Seq(ModelChangeEvent(intModel, 6)), Nil).render().handledEventsIterator.toList + val handledEvents = newController(stringModel, "v", Seq(ModelChangeEvent(intModel, 6)), Nil).model(using intModel)(5).render().handledEventsIterator.toList handledEvents(1).modelOf(intModel) should be(6) test("onModelChange for different model"): From 528ae0825b1398b2717e2ed45afb5f5d6be69dba Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 1 Mar 2024 17:47:19 +0000 Subject: [PATCH 246/313] - --- .../scala/org/terminal21/client/ControllerTest.scala | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 085296d0..617a9266 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -295,3 +295,13 @@ class ControllerTest extends AnyFunSuiteLike: val handledEvents = newController(stringModel, "v", Seq(ModelChangeEvent(intModel, 6)), Seq(b1)).render().handledEventsIterator.toList handledEvents(1).renderedChanges should be(Seq(b1.withText("changed 6"))) + + test("onModelChange when model change triggered by event"): + import Givens.given + val b1 = Button().onModelChangeRender(using intModel): (b, m) => + b.withText(s"changed $m") + val b2 = button.onClick(using intModel): event => + event.handled.mapModel(_ + 1) + val handledEvents = newController(intModel, 5, Seq(buttonClick), Seq(b1, b2)).render().handledEventsIterator.toList + + handledEvents(1).renderedChanges should be(Seq(b1.withText("changed 6"))) From fbc52554b887440b64c66b1f2e9facf744792605 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 1 Mar 2024 17:49:04 +0000 Subject: [PATCH 247/313] - --- .../src/main/scala/org/terminal21/client/Controller.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 613ac278..c38453ec 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -166,7 +166,7 @@ class RenderedController( ) ) - private def renderChangesWhenModelChanges[A]( + private def changesToRenderWhenModelChanges[A]( oldHandled: Handled[A], newHandled: Handled[A], componentsByKey: ComponentsByKey @@ -215,7 +215,7 @@ class RenderedController( val oldHandled = oldHandledEvent.toHandled(model).copy(renderedChanges = Nil) val handled2 = invokeEventHandlers(oldHandled, oldHandledEvent.componentsByKey, event) val handled3 = invokeComponentEventHandlers(handled2, oldHandledEvent.componentsByKey, event) - val (componentsByKey, newHandled) = renderChangesWhenModelChanges(oldHandled, handled3, oldHandledEvent.componentsByKey) + val (componentsByKey, newHandled) = changesToRenderWhenModelChanges(oldHandled, handled3, oldHandledEvent.componentsByKey) if newHandled.renderedChanges.nonEmpty then renderChanges(newHandled.renderedChanges) oldHandledEvent.copy( modelValues = newHandled.modelValues, From 4322fcfcad8920911ef32223989e8a697878c243 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 1 Mar 2024 17:57:01 +0000 Subject: [PATCH 248/313] - --- .../terminal21/client/ControllerTest.scala | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 617a9266..b60cdeed 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -6,8 +6,8 @@ import org.scalatest.funsuite.AnyFunSuiteLike import org.scalatest.matchers.should.Matchers.* import org.scalatestplus.mockito.MockitoSugar.* import org.terminal21.client.components.UiElement -import org.terminal21.client.components.chakra.{Button, Checkbox, QuickTable} -import org.terminal21.client.components.std.Input +import org.terminal21.client.components.chakra.{Box, Button, Checkbox, QuickTable} +import org.terminal21.client.components.std.{Input, Paragraph} import org.terminal21.collections.SEList import org.terminal21.model.{ClientEvent, CommandEvent, OnChange, OnClick} @@ -305,3 +305,18 @@ class ControllerTest extends AnyFunSuiteLike: val handledEvents = newController(intModel, 5, Seq(buttonClick), Seq(b1, b2)).render().handledEventsIterator.toList handledEvents(1).renderedChanges should be(Seq(b1.withText("changed 6"))) + + test("onModelChange hierarchy"): + import Givens.given + val b1 = Button() + .onModelChangeRender(using intModel): (b, m) => + b.withText(s"changed $m") + .onClick(using stringModel): event => // does nothing, just simulates that this button is actually for a different model + event.handled + val b2 = button.onClick(using intModel): event => + event.handled.mapModel(_ + 1) + val box = Box().withChildren(b1, Paragraph().withChildren(b2)) + + val handledEvents = newController(intModel, 5, Seq(buttonClick), Seq(box)).render().handledEventsIterator.toList + + handledEvents(1).renderedChanges should be(Seq(b1.withText("changed 6"))) From 522f978dc7193491a52f8b0a16331d4322af4cac Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Fri, 1 Mar 2024 20:44:13 +0000 Subject: [PATCH 249/313] - --- .../org/terminal21/client/ControllerTest.scala | 13 +++++++++++++ .../client/components/UiElementTest.scala | 4 ++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index b60cdeed..9b3187a5 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -320,3 +320,16 @@ class ControllerTest extends AnyFunSuiteLike: val handledEvents = newController(intModel, 5, Seq(buttonClick), Seq(box)).render().handledEventsIterator.toList handledEvents(1).renderedChanges should be(Seq(b1.withText("changed 6"))) + + test("onModelChange hierarchy with component"): + import Givens.given + val t1 = QuickTable() + .onModelChangeRender(using intModel): (table, m) => + table.withRows(Seq(Seq(s"changed $m"))) + val b2 = button.onClick(using intModel): event => + event.handled.mapModel(_ + 1) + val box = Box().withChildren(t1, Paragraph().withChildren(b2)) + + val handledEvents = newController(intModel, 5, Seq(buttonClick), Seq(box)).render().handledEventsIterator.toList + + handledEvents(1).renderedChanges should be(Seq(t1.withRows(Seq(Seq(s"changed 6"))))) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/components/UiElementTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/components/UiElementTest.scala index f8c952c4..eaa94f86 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/components/UiElementTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/components/UiElementTest.scala @@ -24,5 +24,5 @@ class UiElementTest extends AnyFunSuiteLike: test("substituteComponents when children are component"): val t = QuickTable(key = "k1") - val e = Paragraph().withChildren(t) - e.substituteComponents should be(Paragraph().withChildren(Box("k1", children = t.rendered))) + val e = Paragraph(key = "p1").withChildren(t) + e.substituteComponents should be(Paragraph(key = "p1").withChildren(Box("k1", children = t.rendered))) From ae46f41fd1a3e37e0a5bc48955d121a77dd7a935 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 4 Mar 2024 11:38:10 +0000 Subject: [PATCH 250/313] - --- .../org/terminal21/client/Controller.scala | 1 - .../client/components/chakra/QuickTable.scala | 37 +++++++++++++++---- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index c38453ec..a3d11015 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -123,7 +123,6 @@ class RenderedController( lazy val clickHandlers = clickHandlersMap(allComponents, h) lazy val changeHandlers = changeHandlersMap(allComponents, h) lazy val changeBooleanHandlers = changeBooleanHandlersMap(allComponents, h) - println(event.toString + "/" + h.mm) event match case OnClick(key) if clickHandlers.contains(key) => val handlers = clickHandlers(key) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala index 9f1c883e..7bba698a 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala @@ -11,8 +11,8 @@ case class QuickTable( size: String = "mg", style: Map[String, Any] = Map.empty, caption: Option[String] = None, - headers: Seq[UiElement] = Nil, - rows: Seq[Seq[UiElement]] = Nil, + headers: Seq[Any] = Nil, + rows: Seq[Seq[Any]] = Nil, dataStore: TypedMap = TypedMap.empty ) extends UiComponent with HasStyle: @@ -25,11 +25,35 @@ case class QuickTable( def withCaption(v: String) = copy(caption = Some(v)) override lazy val rendered: Seq[UiElement] = - val head = Thead(key = subKey("th"), children = Seq(Tr(children = headers.map(h => Th(children = Seq(h)))))) + val head = Thead( + key = subKey("thead"), + children = Seq( + Tr( + key = subKey("thead-tr"), + children = headers.zipWithIndex.map: (h, i) => + Th( + key = subKey(s"thead-tr-th-$i"), + children = Seq( + h match + case u: UiElement => u + case c => Text(text = c.toString) + ) + ) + ) + ) + ) val body = Tbody( key = subKey("tb"), children = rows.map: row => - Tr(children = row.map(c => Td(children = Seq(c)))) + Tr(children = row.zipWithIndex.map: (c, i) => + Td( + key = subKey(s"tb-th-$i"), + children = Seq( + c match + case u: UiElement => u + case c => Text(text = c.toString) + ) + )) ) val table = Table( key = subKey("t"), @@ -50,10 +74,7 @@ case class QuickTable( * @return * QuickTable */ - def withRows(data: Seq[Seq[Any]]): QuickTable = copy(rows = data.map(_.map: - case u: UiElement => u - case c => Text(text = c.toString) - )) + def withRows(data: Seq[Seq[Any]]): QuickTable = copy(rows = data) def withRowsElements(data: Seq[Seq[UiElement]]): QuickTable = copy(rows = data) def caption(text: String): QuickTable = copy(caption = Some(text)) From 4c68a648c35b63e09a90d90c8a4ad54cc7c3bf96 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 4 Mar 2024 12:06:21 +0000 Subject: [PATCH 251/313] - --- .../org/terminal21/client/Controller.scala | 17 ++-- .../client/components/EventHandler.scala | 6 +- .../client/components/StdUiCalculation.scala | 4 +- .../client/components/UiElement.scala | 8 +- .../terminal21/client/ControllerTest.scala | 91 +++++++------------ 5 files changed, 50 insertions(+), 76 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index a3d11015..cdd5b5ac 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -21,7 +21,7 @@ class Controller( initialModelValues: Map[Model[Any], Any], eventHandlers: Seq[EventHandler] ): - def model[M](using model: Model[M])(value: M): Controller = + def model[M](model: Model[M], value: M): Controller = new Controller( eventIteratorFactory, renderChanges, @@ -34,7 +34,7 @@ class Controller( initialModelValues .flatMap: (m, v) => components.map: e => - val ne = if e.hasModelChangeRenderHandler(using m) then e.fireModelChangeRender(using m)(v) else e + val ne = if e.hasModelChangeRenderHandler(m) then e.fireModelChangeRender(m)(v) else e ne match case ch: HasChildren => ch.withChildren(applyModelTo(ch.children)*) case x => x @@ -67,10 +67,10 @@ class Controller( object Controller: // def apply[M](initialModel: Model[M], modelComponents: Seq[UiElement])(using session: ConnectedSession): Controller[M] = // new Controller(session.eventIterator, session.renderChanges, modelComponents, initialModel, Nil) - def apply[M](initialValue: M, modelComponents: Seq[UiElement])(using initialModel: Model[M], session: ConnectedSession): Controller = - new Controller(session.eventIterator, session.renderChanges, modelComponents, Map(initialModel.asInstanceOf[Model[Any]] -> initialValue), Nil) - def noModel(modelComponents: Seq[UiElement])(using session: ConnectedSession): Controller = - apply((), modelComponents)(using Model.Standard.unitModel, session) + def apply[M](rootModel: Model[M], initialValue: M, modelComponents: Seq[UiElement])(using session: ConnectedSession): Controller = + new Controller(session.eventIterator, session.renderChanges, modelComponents, Map(rootModel.asInstanceOf[Model[Any]] -> initialValue), Nil) + def noModel(modelComponents: Seq[UiElement])(using session: ConnectedSession): Controller = + apply(Model.Standard.unitModel, (), modelComponents)(using session) class RenderedController( eventIteratorFactory: => Iterator[CommandEvent], @@ -264,7 +264,7 @@ case class HandledEvent( shouldTerminate: Boolean, renderedChanges: Seq[UiElement] ): - def model[A](using model: Model[A]): A = modelOf(model) + def model[A](model: Model[A]): A = modelOf(model) def modelOf[A](model: Model[A]): A = modelValues(model.ModelKey) def toHandled[A](model: Model[A]): Handled[A] = Handled[A](model, modelValues, shouldTerminate, renderedChanges) @@ -285,8 +285,7 @@ object Model: def apply[M: ClassTag]: Model[M] = new Model[M](classTag[M].runtimeClass.getName) def apply[M: ClassTag](name: String): Model[M] = new Model[M](name) object Standard: - given unitModel: Model[Unit] = Model[Unit]("unit") - given booleanModel: Model[Boolean] = Model[Boolean]("boolean") + val unitModel: Model[Unit] = Model[Unit]("unit") case class RenderChangesEvent(changes: Seq[UiElement]) extends ClientEvent case class ModelChangeEvent[M](model: Model[M], newValue: M) extends ClientEvent diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala index cfe020e3..f6d162f1 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala @@ -8,7 +8,7 @@ object OnClickEventHandler: trait CanHandleOnClickEvent: this: UiElement => if key.isEmpty then throw new IllegalStateException(s"clickables must have a stable key. Error occurred on $this") - def onClick[M](using model: Model[M])(h: OnClickEventHandlerFunction[M]): This = + def onClick[M](model: Model[M])(h: OnClickEventHandlerFunction[M]): This = val handlers = dataStore.getOrElse(model.ClickKey, Nil) store(model.ClickKey, handlers :+ h) @@ -16,7 +16,7 @@ object OnChangeEventHandler: trait CanHandleOnChangeEvent: this: UiElement => if key.isEmpty then throw new IllegalStateException(s"changeable must have a stable key. Error occurred on $this") - def onChange[M](using model: Model[M])(h: OnChangeEventHandlerFunction[M]): This = + def onChange[M](model: Model[M])(h: OnChangeEventHandlerFunction[M]): This = val handlers = dataStore.getOrElse(model.ChangeKey, Nil) store(model.ChangeKey, handlers :+ h) @@ -24,6 +24,6 @@ object OnChangeBooleanEventHandler: trait CanHandleOnChangeEvent: this: UiElement => if key.isEmpty then throw new IllegalStateException(s"changeable must have a stable key. Error occurred on $this") - def onChange[M](using model: Model[M])(h: OnChangeBooleanEventHandlerFunction[M]): This = + def onChange[M](model: Model[M])(h: OnChangeBooleanEventHandlerFunction[M]): This = val handlers = dataStore.getOrElse(model.ChangeBooleanKey, Nil) store(model.ChangeBooleanKey, handlers :+ h) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala index 531e2805..74b73063 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala @@ -21,14 +21,14 @@ abstract class StdUiCalculation[OUT]( )(using session: ConnectedSession, executor: FiberExecutor) extends Calculation[OUT] with UiComponent: - import Model.Standard.unitModel + private def model = Model.Standard.unitModel private val running = new AtomicBoolean(false) private val currentUi = new AtomicReference(dataUi) protected def updateUi(dataUi: UiElement & HasStyle) = currentUi.set(dataUi) lazy val badge = Badge() - lazy val recalc = Button(text = "Recalculate", size = Some("sm"), leftIcon = Some(RepeatIcon())).onClick: event => + lazy val recalc = Button(text = "Recalculate", size = Some("sm"), leftIcon = Some(RepeatIcon())).onClick(model): event => import event.* if running.compareAndSet(false, true) then try diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala index 07802113..e108cc32 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala @@ -18,12 +18,12 @@ abstract class UiElement extends AnyElement: /** This handler will be called whenever the model changes. It will also be called with the initial model before the first render() */ - def onModelChangeRender[M](using model: Model[M])(f: (This, M) => This): This = + def onModelChangeRender[M](model: Model[M])(f: (This, M) => This): This = store(UiElementModelsKey, handledModels :+ model).store(model.OnModelChangeRenderKey, f.asInstanceOf[model.OnModelChangeFunction]).asInstanceOf[This] - def hasModelChangeRenderHandler[M](using model: Model[M]): Boolean = dataStore.contains(model.OnModelChangeRenderKey) - def fireModelChangeRender[M](using model: Model[M])(m: M) = + def hasModelChangeRenderHandler[M](model: Model[M]): Boolean = dataStore.contains(model.OnModelChangeRenderKey) + def fireModelChangeRender[M](model: Model[M])(m: M) = dataStore(model.OnModelChangeRenderKey).apply(this, m) - def handledModels: Seq[Model[_]] = dataStore.get(UiElementModelsKey).toSeq.flatten + def handledModels: Seq[Model[_]] = dataStore.get(UiElementModelsKey).toSeq.flatten /** @return * this element along all it's children flattened diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 9b3187a5..46db41b8 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -22,9 +22,8 @@ class ControllerTest extends AnyFunSuiteLike: val checkBoxChange = OnChange(checkbox.key, "true") given ConnectedSession = ConnectedSessionMock.newConnectedSessionMock - object Givens: - given intModel: Model[Int] = Model[Int]("int-model") - given stringModel: Model[String] = Model[String]("string-model") + val intModel: Model[Int] = Model[Int]("int-model") + val stringModel: Model[String] = Model[String]("string-model") def newController[M]( initialModel: Model[M], @@ -37,36 +36,33 @@ class ControllerTest extends AnyFunSuiteLike: val it = seList.iterator events.foreach(e => seList.add(e)) seList.add(CommandEvent.sessionClosed) - new Controller(it, renderChanges, modelComponents, Map.empty, Nil).model(using initialModel)(initialValue) + new Controller(it, renderChanges, modelComponents, Map.empty, Nil).model(initialModel, initialValue) test("will throw an exception if there is a duplicate key"): an[IllegalArgumentException] should be thrownBy newController(Model[Int], 0, Seq(buttonClick), Seq(button, button)).render().handledEventsIterator test("onEvent is called"): - import Givens.intModel newController(intModel, 0, Seq(buttonClick), Seq(button)) .onEvent: case ControllerClickEvent[Int @unchecked](_, handled) => if handled.model > 1 then handled.terminate else handled.withModel(handled.model + 1) .render() .handledEventsIterator - .map(_.model) + .map(_.model(intModel)) .toList should be(List(0, 1)) test("onEvent is called for change"): - import Givens.intModel newController(intModel, 0, Seq(inputChange), Seq(input)) .onEvent: case ControllerChangeEvent[Int @unchecked](_, handled, newValue) => if handled.model > 1 then handled.terminate else handled.withModel(handled.model + 1) .render() .handledEventsIterator - .map(_.model) + .map(_.model(intModel)) .toList should be(List(0, 1)) test("onEvent not matched for change"): - import Givens.intModel newController(intModel, 0, Seq(inputChange), Seq(input)) .onEvent: case event: ControllerClickEvent[Int @unchecked] => @@ -74,11 +70,10 @@ class ControllerTest extends AnyFunSuiteLike: handled.withModel(5) .render() .handledEventsIterator - .map(_.model) + .map(_.model(intModel)) .toList should be(List(0, 0)) test("onEvent is called for change/boolean"): - import Givens.intModel newController(intModel, 0, Seq(checkBoxChange), Seq(checkbox)) .onEvent: case event: ControllerChangeBooleanEvent[Int @unchecked] => @@ -86,11 +81,10 @@ class ControllerTest extends AnyFunSuiteLike: if event.model > 1 then handled.terminate else handled.withModel(event.model + 1) .render() .handledEventsIterator - .map(_.model) + .map(_.model(intModel)) .toList should be(List(0, 1)) test("onEvent not matches for change/boolean"): - import Givens.intModel newController(intModel, 0, Seq(checkBoxChange), Seq(checkbox)) .onEvent: case event: ControllerClickEvent[Int @unchecked] => @@ -98,94 +92,87 @@ class ControllerTest extends AnyFunSuiteLike: handled.withModel(5) .render() .handledEventsIterator - .map(_.model) + .map(_.model(intModel)) .toList should be(List(0, 0)) case class TestClientEvent(i: Int) extends ClientEvent test("onEvent is called for ClientEvent"): - import Givens.intModel newController(intModel, 0, Seq(TestClientEvent(5)), Seq(button)) .onEvent: case ControllerClientEvent[Int @unchecked](handled, event: TestClientEvent) => handled.withModel(event.i).terminate .render() .handledEventsIterator - .map(_.model) + .map(_.model(intModel)) .toList should be(List(0, 5)) test("onEvent when no partial function matches ClientEvent"): - import Givens.intModel newController(intModel, 0, Seq(TestClientEvent(5)), Seq(button)) .onEvent: case ControllerClickEvent[Int @unchecked](`checkbox`, handled) => handled.withModel(5).terminate .render() .handledEventsIterator - .map(_.model) + .map(_.model(intModel)) .toList should be(List(0, 0)) test("onClick is called"): - import Givens.intModel newController( intModel, 0, Seq(buttonClick), Seq( - button.onClick: event => + button.onClick(intModel): event => event.handled.withModel(100).terminate ) ).render() .handledEventsIterator - .map(_.model) + .map(_.model(intModel)) .toList should be(List(0, 100)) test("onChange is called"): - import Givens.intModel newController( intModel, 0, Seq(inputChange), Seq( - input.onChange: event => + input.onChange(intModel): event => event.handled.withModel(100).terminate ) ).render() .handledEventsIterator - .map(_.model) + .map(_.model(intModel)) .toList should be(List(0, 100)) test("onChange/boolean is called"): - import Givens.intModel newController( intModel, 0, Seq(checkBoxChange), Seq( - checkbox.onChange: event => + checkbox.onChange(intModel): event => event.handled.withModel(100).terminate ) ).render() .handledEventsIterator - .map(_.model) + .map(_.model(intModel)) .toList should be(List(0, 100)) test("terminate is obeyed and latest model state is iterated"): - import Givens.intModel newController(intModel, 0, Seq(buttonClick, buttonClick, buttonClick), Seq(button)) .onEvent: case event: ControllerEvent[Int @unchecked] => if event.handled.model > 1 then event.handled.terminate.withModel(100) else event.handled.withModel(event.handled.model + 1) .render() .handledEventsIterator - .map(_.model) + .map(_.model(intModel)) .toList should be(List(0, 1, 2, 100)) test("changes are rendered"): - import Givens.intModel var rendered = Seq.empty[UiElement] def renderer(s: Seq[UiElement]): Unit = rendered = s - val but = button.onModelChangeRender: (b, m) => + val but = button.onModelChangeRender(intModel): (b, m) => b.withText(s"changed $m") val handled = newController(intModel, 0, Seq(buttonClick), Seq(but), renderer) @@ -201,8 +188,7 @@ class ControllerTest extends AnyFunSuiteLike: handled.map(_.renderedChanges)(1) should be(expected) test("rendered are cleared"): - import Givens.intModel - val but = button.onModelChangeRender: (b, m) => + val but = button.onModelChangeRender(intModel): (b, m) => if m == 1 then b.withText(s"changed $m") else b val handled = newController(intModel, 0, Seq(buttonClick, checkBoxChange), Seq(but, checkbox)) @@ -220,11 +206,10 @@ class ControllerTest extends AnyFunSuiteLike: rendered(2) should be(Nil) test("components handle events"): - import Givens.intModel val table = QuickTable().withRows( Seq( Seq( - button.onClick: event => + button.onClick(intModel): event => import event.* handled.withModel(model + 1).terminate ) @@ -235,22 +220,21 @@ class ControllerTest extends AnyFunSuiteLike: .handledEventsIterator .toList - handledEvents.map(_.model) should be(List(0, 1)) + handledEvents.map(_.model(intModel)) should be(List(0, 1)) test("components receive onModelChange"): - import Givens.intModel val called = new AtomicBoolean(false) val table = QuickTable() .withRows( Seq( Seq( - button.onClick: event => + button.onClick(intModel): event => import event.* handled.withModel(model + 1).terminate ) ) ) - .onModelChangeRender: (table, _) => + .onModelChangeRender(intModel): (table, _) => called.set(true) table newController(intModel, 0, Seq(buttonClick), Seq(table)) @@ -261,9 +245,7 @@ class ControllerTest extends AnyFunSuiteLike: called.get() should be(true) test("applies initial model before rendering"): - import Givens.intModel - - val b = button.onModelChangeRender: (b, m) => + val b = button.onModelChangeRender(intModel): (b, m) => b.withText(s"model $m") val connectedSession = mock[ConnectedSession] @@ -273,8 +255,6 @@ class ControllerTest extends AnyFunSuiteLike: verify(connectedSession).render(Seq(b.withText("model 5"))) test("RenderChangesEvent renders changes"): - import Givens.intModel - val handledEvents = newController(intModel, 5, Seq(RenderChangesEvent(Seq(button.withText("changed")))), Seq(button)) .render() .handledEventsIterator @@ -283,13 +263,11 @@ class ControllerTest extends AnyFunSuiteLike: handledEvents(1).renderedChanges should be(Seq(button.withText("changed"))) test("ModelChangeEvent"): - import Givens.given - val handledEvents = newController(stringModel, "v", Seq(ModelChangeEvent(intModel, 6)), Nil).model(using intModel)(5).render().handledEventsIterator.toList + val handledEvents = newController(stringModel, "v", Seq(ModelChangeEvent(intModel, 6)), Nil).model(intModel, 5).render().handledEventsIterator.toList handledEvents(1).modelOf(intModel) should be(6) test("onModelChange for different model"): - import Givens.given - val b1 = button.onModelChangeRender(using intModel): (b, m) => + val b1 = button.onModelChangeRender(intModel): (b, m) => b.withText(s"changed $m") val handledEvents = newController(stringModel, "v", Seq(ModelChangeEvent(intModel, 6)), Seq(b1)).render().handledEventsIterator.toList @@ -297,23 +275,21 @@ class ControllerTest extends AnyFunSuiteLike: handledEvents(1).renderedChanges should be(Seq(b1.withText("changed 6"))) test("onModelChange when model change triggered by event"): - import Givens.given - val b1 = Button().onModelChangeRender(using intModel): (b, m) => + val b1 = Button().onModelChangeRender(intModel): (b, m) => b.withText(s"changed $m") - val b2 = button.onClick(using intModel): event => + val b2 = button.onClick(intModel): event => event.handled.mapModel(_ + 1) val handledEvents = newController(intModel, 5, Seq(buttonClick), Seq(b1, b2)).render().handledEventsIterator.toList handledEvents(1).renderedChanges should be(Seq(b1.withText("changed 6"))) test("onModelChange hierarchy"): - import Givens.given val b1 = Button() - .onModelChangeRender(using intModel): (b, m) => + .onModelChangeRender(intModel): (b, m) => b.withText(s"changed $m") - .onClick(using stringModel): event => // does nothing, just simulates that this button is actually for a different model + .onClick(stringModel): event => // does nothing, just simulates that this button is actually for a different model event.handled - val b2 = button.onClick(using intModel): event => + val b2 = button.onClick(intModel): event => event.handled.mapModel(_ + 1) val box = Box().withChildren(b1, Paragraph().withChildren(b2)) @@ -322,11 +298,10 @@ class ControllerTest extends AnyFunSuiteLike: handledEvents(1).renderedChanges should be(Seq(b1.withText("changed 6"))) test("onModelChange hierarchy with component"): - import Givens.given val t1 = QuickTable() - .onModelChangeRender(using intModel): (table, m) => + .onModelChangeRender(intModel): (table, m) => table.withRows(Seq(Seq(s"changed $m"))) - val b2 = button.onClick(using intModel): event => + val b2 = button.onClick(intModel): event => event.handled.mapModel(_ + 1) val box = Box().withChildren(t1, Paragraph().withChildren(b2)) From 7cf5946ac993ef73885261457bd0081c1b0db2f5 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 4 Mar 2024 12:28:17 +0000 Subject: [PATCH 252/313] - --- .../org/terminal21/client/Controller.scala | 11 +++--- .../terminal21/client/ControllerTest.scala | 36 +++++++++++++++++-- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index cdd5b5ac..d9d8caeb 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -21,7 +21,7 @@ class Controller( initialModelValues: Map[Model[Any], Any], eventHandlers: Seq[EventHandler] ): - def model[M](model: Model[M], value: M): Controller = + def withModel[M](model: Model[M], value: M): Controller = new Controller( eventIteratorFactory, renderChanges, @@ -31,14 +31,13 @@ class Controller( ) private def applyModelTo(components: Seq[UiElement]): Seq[UiElement] = - initialModelValues - .flatMap: (m, v) => - components.map: e => + components.map: c => + initialModelValues.foldLeft(c): + case (e, (m, v)) => val ne = if e.hasModelChangeRenderHandler(m) then e.fireModelChangeRender(m)(v) else e ne match case ch: HasChildren => ch.withChildren(applyModelTo(ch.children)*) case x => x - .toList def render()(using session: ConnectedSession): RenderedController = val elements = applyModelTo(modelComponents) @@ -147,7 +146,7 @@ class RenderedController( case _ => h private def checkForDuplicatesAndThrow(components: Seq[UiElement]): Unit = - val duplicates = components.map(_.key).groupBy(identity).filter(_._2.size > 1).keys.toSet + val duplicates = components.groupBy(_.key).filter(_._2.size > 1).keys.toSet if duplicates.nonEmpty then val duplicateComponents = components.filter(e => duplicates.contains(e.key)) throw new IllegalArgumentException(s"Duplicate(s) found: ${duplicates.mkString(", ")}\nDuplicate components:\n${duplicateComponents.mkString("\n")}") diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 46db41b8..6c869424 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -36,7 +36,7 @@ class ControllerTest extends AnyFunSuiteLike: val it = seList.iterator events.foreach(e => seList.add(e)) seList.add(CommandEvent.sessionClosed) - new Controller(it, renderChanges, modelComponents, Map.empty, Nil).model(initialModel, initialValue) + new Controller(it, renderChanges, modelComponents, Map.empty, Nil).withModel(initialModel, initialValue) test("will throw an exception if there is a duplicate key"): an[IllegalArgumentException] should be thrownBy @@ -131,6 +131,24 @@ class ControllerTest extends AnyFunSuiteLike: .map(_.model(intModel)) .toList should be(List(0, 100)) + test("onClick is called for multiple models"): + newController( + intModel, + 0, + Seq(buttonClick), + Seq( + button + .onClick(intModel): event => + event.handled.withModel(100).terminate + .onClick(stringModel): event => + event.handled.withModel("new").terminate + ) + ).withModel(stringModel, "old") + .render() + .handledEventsIterator + .map(h => (h.model(intModel), h.model(stringModel))) + .toList should be(List((0, "old"), (100, "new"))) + test("onChange is called"): newController( intModel, @@ -254,6 +272,20 @@ class ControllerTest extends AnyFunSuiteLike: verify(connectedSession).render(Seq(b.withText("model 5"))) + test("applies multiple initial model before rendering"): + val b = button + .onModelChangeRender(intModel): (b, m) => + b.withText(s"model $m") + .onModelChangeRender(stringModel): (b, m) => + b.withText(b.text + s" model $m") + + val connectedSession = mock[ConnectedSession] + newController(intModel, 5, Nil, Seq(b)) + .withModel(stringModel, "x") + .render()(using connectedSession) + + verify(connectedSession).render(Seq(b.withText("model 5 model x"))) + test("RenderChangesEvent renders changes"): val handledEvents = newController(intModel, 5, Seq(RenderChangesEvent(Seq(button.withText("changed")))), Seq(button)) .render() @@ -263,7 +295,7 @@ class ControllerTest extends AnyFunSuiteLike: handledEvents(1).renderedChanges should be(Seq(button.withText("changed"))) test("ModelChangeEvent"): - val handledEvents = newController(stringModel, "v", Seq(ModelChangeEvent(intModel, 6)), Nil).model(intModel, 5).render().handledEventsIterator.toList + val handledEvents = newController(stringModel, "v", Seq(ModelChangeEvent(intModel, 6)), Nil).withModel(intModel, 5).render().handledEventsIterator.toList handledEvents(1).modelOf(intModel) should be(6) test("onModelChange for different model"): From 17a5e1f133e02e4f903e8ce0daa3741e17471529 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 4 Mar 2024 12:33:21 +0000 Subject: [PATCH 253/313] - --- .../test/scala/org/terminal21/client/ControllerTest.scala | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 6c869424..06ee73d6 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -306,6 +306,14 @@ class ControllerTest extends AnyFunSuiteLike: handledEvents(1).renderedChanges should be(Seq(b1.withText("changed 6"))) + test("onModelChange but no change to element"): + val b1 = button.onModelChangeRender(intModel): (b, m) => + b + + val handledEvents = newController(stringModel, "v", Seq(ModelChangeEvent(intModel, 6)), Seq(b1)).render().handledEventsIterator.toList + + handledEvents(1).renderedChanges should be(Nil) + test("onModelChange when model change triggered by event"): val b1 = Button().onModelChangeRender(intModel): (b, m) => b.withText(s"changed $m") From 1382b9b52ee7977b6fa83b5b4fd4307eac03f975 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 4 Mar 2024 12:35:45 +0000 Subject: [PATCH 254/313] - --- .../src/test/scala/org/terminal21/client/ControllerTest.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 06ee73d6..56c966b3 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -307,7 +307,7 @@ class ControllerTest extends AnyFunSuiteLike: handledEvents(1).renderedChanges should be(Seq(b1.withText("changed 6"))) test("onModelChange but no change to element"): - val b1 = button.onModelChangeRender(intModel): (b, m) => + val b1 = button.onModelChangeRender(intModel): (b, _) => b val handledEvents = newController(stringModel, "v", Seq(ModelChangeEvent(intModel, 6)), Seq(b1)).render().handledEventsIterator.toList From e0ed040af9f09f70b6b0d18b75b2e4eed196741b Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 4 Mar 2024 12:41:04 +0000 Subject: [PATCH 255/313] - --- .../scala/org/terminal21/client/Controller.scala | 14 +++++++------- .../client/components/EventHandler.scala | 12 ++++++------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index d9d8caeb..de650b30 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -101,20 +101,20 @@ class RenderedController( private def clickHandlersMap[A](allComponents: Seq[UiElement], h: Handled[A]): Map[String, Seq[OnClickEventHandlerFunction[A]]] = allComponents .collect: - case e: OnClickEventHandler.CanHandleOnClickEvent if e.dataStore.contains(h.mm.ClickKey) => (e.key, e.dataStore(h.mm.ClickKey)) + case e: OnClickEventHandler.CanHandleOnClickEvent if e.dataStore.contains(h.mm.ClickEventHandlerKey) => (e.key, e.dataStore(h.mm.ClickEventHandlerKey)) .toMap private def changeHandlersMap[A](allComponents: Seq[UiElement], h: Handled[A]): Map[String, Seq[OnChangeEventHandlerFunction[A]]] = allComponents .collect: - case e: OnChangeEventHandler.CanHandleOnChangeEvent if e.dataStore.contains(h.mm.ChangeKey) => (e.key, e.dataStore(h.mm.ChangeKey)) + case e: OnChangeEventHandler.CanHandleOnChangeEvent if e.dataStore.contains(h.mm.ChangeEventHandlerKey) => (e.key, e.dataStore(h.mm.ChangeEventHandlerKey)) .toMap private def changeBooleanHandlersMap[A](allComponents: Seq[UiElement], h: Handled[A]): Map[String, Seq[OnChangeBooleanEventHandlerFunction[A]]] = allComponents .collect: - case e: OnChangeBooleanEventHandler.CanHandleOnChangeEvent if e.dataStore.contains(h.mm.ChangeBooleanKey) => - (e.key, e.dataStore(h.mm.ChangeBooleanKey)) + case e: OnChangeBooleanEventHandler.CanHandleOnChangeEvent if e.dataStore.contains(h.mm.ChangeBooleanEventHandlerKey) => + (e.key, e.dataStore(h.mm.ChangeBooleanEventHandlerKey)) .toMap private def invokeComponentEventHandlers[A](h: Handled[A], componentsByKey: ComponentsByKey, event: CommandEvent): Handled[A] = @@ -275,9 +275,9 @@ class Model[M: ClassTag](name: String): type OnModelChangeFunction = (UiElement, M) => UiElement object ModelKey extends TypedMapKey[M] object OnModelChangeRenderKey extends TypedMapKey[OnModelChangeFunction] - object ClickKey extends TypedMapKey[Seq[OnClickEventHandlerFunction[M]]] - object ChangeKey extends TypedMapKey[Seq[OnChangeEventHandlerFunction[M]]] - object ChangeBooleanKey extends TypedMapKey[Seq[OnChangeBooleanEventHandlerFunction[M]]] + object ClickEventHandlerKey extends TypedMapKey[Seq[OnClickEventHandlerFunction[M]]] + object ChangeEventHandlerKey extends TypedMapKey[Seq[OnChangeEventHandlerFunction[M]]] + object ChangeBooleanEventHandlerKey extends TypedMapKey[Seq[OnChangeBooleanEventHandlerFunction[M]]] override def toString = s"Model($name)" object Model: diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala index f6d162f1..4abaeee9 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala @@ -9,21 +9,21 @@ object OnClickEventHandler: this: UiElement => if key.isEmpty then throw new IllegalStateException(s"clickables must have a stable key. Error occurred on $this") def onClick[M](model: Model[M])(h: OnClickEventHandlerFunction[M]): This = - val handlers = dataStore.getOrElse(model.ClickKey, Nil) - store(model.ClickKey, handlers :+ h) + val handlers = dataStore.getOrElse(model.ClickEventHandlerKey, Nil) + store(model.ClickEventHandlerKey, handlers :+ h) object OnChangeEventHandler: trait CanHandleOnChangeEvent: this: UiElement => if key.isEmpty then throw new IllegalStateException(s"changeable must have a stable key. Error occurred on $this") def onChange[M](model: Model[M])(h: OnChangeEventHandlerFunction[M]): This = - val handlers = dataStore.getOrElse(model.ChangeKey, Nil) - store(model.ChangeKey, handlers :+ h) + val handlers = dataStore.getOrElse(model.ChangeEventHandlerKey, Nil) + store(model.ChangeEventHandlerKey, handlers :+ h) object OnChangeBooleanEventHandler: trait CanHandleOnChangeEvent: this: UiElement => if key.isEmpty then throw new IllegalStateException(s"changeable must have a stable key. Error occurred on $this") def onChange[M](model: Model[M])(h: OnChangeBooleanEventHandlerFunction[M]): This = - val handlers = dataStore.getOrElse(model.ChangeBooleanKey, Nil) - store(model.ChangeBooleanKey, handlers :+ h) + val handlers = dataStore.getOrElse(model.ChangeBooleanEventHandlerKey, Nil) + store(model.ChangeBooleanEventHandlerKey, handlers :+ h) From 592c6f48e844fcb3e55635df81e2b010c0bd46fc Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 4 Mar 2024 15:13:47 +0000 Subject: [PATCH 256/313] - --- .../org/terminal21/client/Controller.scala | 18 +++++++----- .../terminal21/client/ControllerTest.scala | 29 ++++++++++++++++++- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index de650b30..21c976cb 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -107,7 +107,8 @@ class RenderedController( private def changeHandlersMap[A](allComponents: Seq[UiElement], h: Handled[A]): Map[String, Seq[OnChangeEventHandlerFunction[A]]] = allComponents .collect: - case e: OnChangeEventHandler.CanHandleOnChangeEvent if e.dataStore.contains(h.mm.ChangeEventHandlerKey) => (e.key, e.dataStore(h.mm.ChangeEventHandlerKey)) + case e: OnChangeEventHandler.CanHandleOnChangeEvent if e.dataStore.contains(h.mm.ChangeEventHandlerKey) => + (e.key, e.dataStore(h.mm.ChangeEventHandlerKey)) .toMap private def changeBooleanHandlersMap[A](allComponents: Seq[UiElement], h: Handled[A]): Map[String, Seq[OnChangeBooleanEventHandlerFunction[A]]] = @@ -208,19 +209,20 @@ class RenderedController( .scanLeft(initHandledEvent): case (ohEvent, event) => try - ohEvent.models.foldLeft(ohEvent): + val nhEvent = ohEvent.models.foldLeft(ohEvent): case (oldHandledEvent, model) => val oldHandled = oldHandledEvent.toHandled(model).copy(renderedChanges = Nil) val handled2 = invokeEventHandlers(oldHandled, oldHandledEvent.componentsByKey, event) val handled3 = invokeComponentEventHandlers(handled2, oldHandledEvent.componentsByKey, event) val (componentsByKey, newHandled) = changesToRenderWhenModelChanges(oldHandled, handled3, oldHandledEvent.componentsByKey) - if newHandled.renderedChanges.nonEmpty then renderChanges(newHandled.renderedChanges) oldHandledEvent.copy( modelValues = newHandled.modelValues, componentsByKey = componentsByKey, shouldTerminate = newHandled.shouldTerminate, renderedChanges = newHandled.renderedChanges ) + if nhEvent.renderedChanges.nonEmpty then renderChanges(nhEvent.renderedChanges) + nhEvent catch case t: Throwable => logger.error("an error occurred while iterating events", t) @@ -273,11 +275,11 @@ type OnChangeBooleanEventHandlerFunction[M] = ControllerChangeBooleanEvent[M] => class Model[M: ClassTag](name: String): type OnModelChangeFunction = (UiElement, M) => UiElement - object ModelKey extends TypedMapKey[M] - object OnModelChangeRenderKey extends TypedMapKey[OnModelChangeFunction] - object ClickEventHandlerKey extends TypedMapKey[Seq[OnClickEventHandlerFunction[M]]] - object ChangeEventHandlerKey extends TypedMapKey[Seq[OnChangeEventHandlerFunction[M]]] - object ChangeBooleanEventHandlerKey extends TypedMapKey[Seq[OnChangeBooleanEventHandlerFunction[M]]] + object ModelKey extends TypedMapKey[M] + object OnModelChangeRenderKey extends TypedMapKey[OnModelChangeFunction] + object ClickEventHandlerKey extends TypedMapKey[Seq[OnClickEventHandlerFunction[M]]] + object ChangeEventHandlerKey extends TypedMapKey[Seq[OnChangeEventHandlerFunction[M]]] + object ChangeBooleanEventHandlerKey extends TypedMapKey[Seq[OnChangeBooleanEventHandlerFunction[M]]] override def toString = s"Model($name)" object Model: diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 56c966b3..0f8ec202 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -6,7 +6,7 @@ import org.scalatest.funsuite.AnyFunSuiteLike import org.scalatest.matchers.should.Matchers.* import org.scalatestplus.mockito.MockitoSugar.* import org.terminal21.client.components.UiElement -import org.terminal21.client.components.chakra.{Box, Button, Checkbox, QuickTable} +import org.terminal21.client.components.chakra.{Box, Button, Checkbox, QuickTable, Text} import org.terminal21.client.components.std.{Input, Paragraph} import org.terminal21.collections.SEList import org.terminal21.model.{ClientEvent, CommandEvent, OnChange, OnClick} @@ -348,3 +348,30 @@ class ControllerTest extends AnyFunSuiteLike: val handledEvents = newController(intModel, 5, Seq(buttonClick), Seq(box)).render().handledEventsIterator.toList handledEvents(1).renderedChanges should be(Seq(t1.withRows(Seq(Seq(s"changed 6"))))) + + test("onChildModelChange"): + case class Person(id: Int, name: String) + class PersonComponent(person: Person): + val m = Model[Person]("person") + val component = Box() + .withChildren( + Text(text = "Name"), + Input(s"person-${person.id}", defaultValue = person.name) + .onChange(m): event => + import event.* + handled.withModel(model.copy(name = newValue)) + ) + class PeopleComponent(people: Seq[Person]): + val m = Model[Seq[Person]]("people-model") + + val component = QuickTable() + .onModelChangeRender(m): (t, people) => + val peopleComponents = people.map(p => new PersonComponent(p)) + t.withRows(peopleComponents.map(p => Seq(p.component))) + + val people = Seq(Person(10, "person 1"), Person(20, "person 2")) + val peopleComponent = new PeopleComponent(people) + val session = mock[ConnectedSession] + newController(peopleComponent.m, people, Nil, Seq(peopleComponent.component)) + .render()(using session) + verify(session).render(Seq(peopleComponent.component.withRows(Nil))) From f8200f170b4cda35c32a904187c371e8d6ff8969 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 4 Mar 2024 15:48:30 +0000 Subject: [PATCH 257/313] - --- .../test/scala/org/terminal21/client/ControllerTest.scala | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 0f8ec202..7e9f44d2 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -364,10 +364,9 @@ class ControllerTest extends AnyFunSuiteLike: class PeopleComponent(people: Seq[Person]): val m = Model[Seq[Person]]("people-model") - val component = QuickTable() - .onModelChangeRender(m): (t, people) => - val peopleComponents = people.map(p => new PersonComponent(p)) - t.withRows(peopleComponents.map(p => Seq(p.component))) + val peopleComponents = people.map(p => new PersonComponent(p)) + val component = QuickTable("people") + .withRows(peopleComponents.map(p => Seq(p.component))) val people = Seq(Person(10, "person 1"), Person(20, "person 2")) val peopleComponent = new PeopleComponent(people) From 2e2749f8fa6074e81da0e5452f482b4a03a8cf06 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 4 Mar 2024 18:01:19 +0000 Subject: [PATCH 258/313] - --- .../org/terminal21/client/Controller.scala | 23 +++++++------------ .../terminal21/client/ControllerTest.scala | 4 ++-- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 21c976cb..072b6c22 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -14,31 +14,24 @@ import scala.reflect.{ClassTag, classTag} type EventHandler = PartialFunction[ControllerEvent[_], Handled[_]] type ComponentsByKey = Map[String, UiElement] -class Controller( +class Controller[M]( eventIteratorFactory: => Iterator[CommandEvent], renderChanges: Seq[UiElement] => Unit, - modelComponents: Seq[UiElement], + initialModelValue: M, + modelComponents: M => Seq[UiElement], initialModelValues: Map[Model[Any], Any], eventHandlers: Seq[EventHandler] ): - def withModel[M](model: Model[M], value: M): Controller = + def withModel[M2](model: Model[M2], value: M2): Controller[M] = new Controller( eventIteratorFactory, renderChanges, + initialModel, modelComponents, initialModelValues + (model.asInstanceOf[Model[Any]] -> value), eventHandlers ) - private def applyModelTo(components: Seq[UiElement]): Seq[UiElement] = - components.map: c => - initialModelValues.foldLeft(c): - case (e, (m, v)) => - val ne = if e.hasModelChangeRenderHandler(m) then e.fireModelChangeRender(m)(v) else e - ne match - case ch: HasChildren => ch.withChildren(applyModelTo(ch.children)*) - case x => x - def render()(using session: ConnectedSession): RenderedController = val elements = applyModelTo(modelComponents) session.render(elements) @@ -273,7 +266,7 @@ type OnClickEventHandlerFunction[M] = ControllerClickEvent[M] => Handled type OnChangeEventHandlerFunction[M] = ControllerChangeEvent[M] => Handled[M] type OnChangeBooleanEventHandlerFunction[M] = ControllerChangeBooleanEvent[M] => Handled[M] -class Model[M: ClassTag](name: String): +class Model[M: ClassTag](name: String, initialValue: M): type OnModelChangeFunction = (UiElement, M) => UiElement object ModelKey extends TypedMapKey[M] object OnModelChangeRenderKey extends TypedMapKey[OnModelChangeFunction] @@ -283,8 +276,8 @@ class Model[M: ClassTag](name: String): override def toString = s"Model($name)" object Model: - def apply[M: ClassTag]: Model[M] = new Model[M](classTag[M].runtimeClass.getName) - def apply[M: ClassTag](name: String): Model[M] = new Model[M](name) + def apply[M: ClassTag](value: M): Model[M] = new Model[M](classTag[M].runtimeClass.getName, value) + def apply[M: ClassTag](name: String, value: M): Model[M] = new Model[M](name, value) object Standard: val unitModel: Model[Unit] = Model[Unit]("unit") diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 7e9f44d2..f2dc0a15 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -352,7 +352,7 @@ class ControllerTest extends AnyFunSuiteLike: test("onChildModelChange"): case class Person(id: Int, name: String) class PersonComponent(person: Person): - val m = Model[Person]("person") + val m = Model[Person](person) val component = Box() .withChildren( Text(text = "Name"), @@ -362,7 +362,7 @@ class ControllerTest extends AnyFunSuiteLike: handled.withModel(model.copy(name = newValue)) ) class PeopleComponent(people: Seq[Person]): - val m = Model[Seq[Person]]("people-model") + val m = Model[Seq[Person]]("people-model", people) val peopleComponents = people.map(p => new PersonComponent(p)) val component = QuickTable("people") From 40609b2ebda0577e8fb0ce4235a665d5664d0479 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 4 Mar 2024 19:30:30 +0000 Subject: [PATCH 259/313] - --- .../org/terminal21/client/Controller.scala | 23 ++++++++---- .../terminal21/client/ControllerTest.scala | 37 ++++++++++--------- 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 072b6c22..21c976cb 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -14,24 +14,31 @@ import scala.reflect.{ClassTag, classTag} type EventHandler = PartialFunction[ControllerEvent[_], Handled[_]] type ComponentsByKey = Map[String, UiElement] -class Controller[M]( +class Controller( eventIteratorFactory: => Iterator[CommandEvent], renderChanges: Seq[UiElement] => Unit, - initialModelValue: M, - modelComponents: M => Seq[UiElement], + modelComponents: Seq[UiElement], initialModelValues: Map[Model[Any], Any], eventHandlers: Seq[EventHandler] ): - def withModel[M2](model: Model[M2], value: M2): Controller[M] = + def withModel[M](model: Model[M], value: M): Controller = new Controller( eventIteratorFactory, renderChanges, - initialModel, modelComponents, initialModelValues + (model.asInstanceOf[Model[Any]] -> value), eventHandlers ) + private def applyModelTo(components: Seq[UiElement]): Seq[UiElement] = + components.map: c => + initialModelValues.foldLeft(c): + case (e, (m, v)) => + val ne = if e.hasModelChangeRenderHandler(m) then e.fireModelChangeRender(m)(v) else e + ne match + case ch: HasChildren => ch.withChildren(applyModelTo(ch.children)*) + case x => x + def render()(using session: ConnectedSession): RenderedController = val elements = applyModelTo(modelComponents) session.render(elements) @@ -266,7 +273,7 @@ type OnClickEventHandlerFunction[M] = ControllerClickEvent[M] => Handled type OnChangeEventHandlerFunction[M] = ControllerChangeEvent[M] => Handled[M] type OnChangeBooleanEventHandlerFunction[M] = ControllerChangeBooleanEvent[M] => Handled[M] -class Model[M: ClassTag](name: String, initialValue: M): +class Model[M: ClassTag](name: String): type OnModelChangeFunction = (UiElement, M) => UiElement object ModelKey extends TypedMapKey[M] object OnModelChangeRenderKey extends TypedMapKey[OnModelChangeFunction] @@ -276,8 +283,8 @@ class Model[M: ClassTag](name: String, initialValue: M): override def toString = s"Model($name)" object Model: - def apply[M: ClassTag](value: M): Model[M] = new Model[M](classTag[M].runtimeClass.getName, value) - def apply[M: ClassTag](name: String, value: M): Model[M] = new Model[M](name, value) + def apply[M: ClassTag]: Model[M] = new Model[M](classTag[M].runtimeClass.getName) + def apply[M: ClassTag](name: String): Model[M] = new Model[M](name) object Standard: val unitModel: Model[Unit] = Model[Unit]("unit") diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index f2dc0a15..f48c7fdf 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -350,27 +350,30 @@ class ControllerTest extends AnyFunSuiteLike: handledEvents(1).renderedChanges should be(Seq(t1.withRows(Seq(Seq(s"changed 6"))))) test("onChildModelChange"): + case class Events(key: String): + def changedValue(e: UiElement): Option[String] = ??? + case class MV[M](model: M, view: UiElement) + case class Person(id: Int, name: String) - class PersonComponent(person: Person): - val m = Model[Person](person) + def personComponent(person: Person, events: Events): MV[Person] = + val nameInput = Input(s"person-${person.id}", defaultValue = person.name) val component = Box() .withChildren( Text(text = "Name"), - Input(s"person-${person.id}", defaultValue = person.name) - .onChange(m): event => - import event.* - handled.withModel(model.copy(name = newValue)) + nameInput ) - class PeopleComponent(people: Seq[Person]): - val m = Model[Seq[Person]]("people-model", people) + MV( + person.copy( + name = events.changedValue(nameInput).getOrElse(person.name) + ), + component + ) - val peopleComponents = people.map(p => new PersonComponent(p)) + def peopleComponent(people: Seq[Person], events: Events): MV[Seq[Person]] = + val peopleComponents = people.map(p => personComponent(p, events)) val component = QuickTable("people") - .withRows(peopleComponents.map(p => Seq(p.component))) - - val people = Seq(Person(10, "person 1"), Person(20, "person 2")) - val peopleComponent = new PeopleComponent(people) - val session = mock[ConnectedSession] - newController(peopleComponent.m, people, Nil, Seq(peopleComponent.component)) - .render()(using session) - verify(session).render(Seq(peopleComponent.component.withRows(Nil))) + .withRows(peopleComponents.map(p => Seq(p.view))) + MV(peopleComponents.map(_.model), component) + + val people = Seq(Person(10, "person 1"), Person(20, "person 2")) + val pc = peopleComponent(people, Events("x")) From 9815ae126fe059d19195968dcd1c03974163fc1c Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 4 Mar 2024 21:18:15 +0000 Subject: [PATCH 260/313] - --- .../serverapp/bundled/AppManager.scala | 66 ++-- .../serverapp/bundled/ServerStatusApp.scala | 87 +++-- .../serverapp/bundled/SettingsApp.scala | 5 +- .../bundled/AppManagerPageTest.scala | 126 +++---- .../bundled/ServerStatusPageTest.scala | 138 +++---- .../terminal21/client/ConnectedSession.scala | 4 +- .../org/terminal21/client/Controller.scala | 319 +++------------- .../client/components/EventHandler.scala | 11 - .../terminal21/client/components/Keys.scala | 3 +- .../client/components/StdUiCalculation.scala | 178 ++++----- .../client/components/UiElement.scala | 14 +- .../terminal21/client/ControllerTest.scala | 342 +----------------- 12 files changed, 356 insertions(+), 937 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala index aa9a3b49..da1ca899 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala @@ -25,57 +25,57 @@ class AppManager(serverSideSessions: ServerSideSessions, fiberExecutor: FiberExe class AppManagerPage(apps: Seq[ServerSideApp], startApp: ServerSideApp => Unit)(using session: ConnectedSession): case class ManagerModel(startApp: Option[ServerSideApp] = None) - given Model[ManagerModel] = Model(ManagerModel()) def run(): Unit = eventsIterator.foreach(_ => ()) - val appRows: Seq[Seq[UiElement]] = apps.map: app => - Seq( - Link(key = s"app-${app.name}", text = app.name).onClick: event => - import event.* - handled.withModel(model.copy(startApp = Some(app))) - , - Text(text = app.description) + def appRows(events: Events): Seq[MV[Option[ServerSideApp]]] = apps.map: app => + val link = Link(key = s"app-${app.name}", text = app.name) + MV( + if events.isClicked(link) then Some(app) else None, + Box().withChildren( + link, + Text(text = app.description) + ) ) - def components: Seq[UiElement] = + def components(model: ManagerModel, events: Events): MV[ManagerModel] = + val appsMv = appRows(events) val appsTable = QuickTable( key = "apps-table", caption = Some("Apps installed on the server, click one to run it."), - rows = appRows + rows = appsMv.map(m => Seq(m.view)) ).withHeaders("App Name", "Description") - - Seq( - Header1(text = "Terminal 21 Manager"), - Paragraph( - text = """ - |Here you can run all the installed apps on the server.""".stripMargin - ), - appsTable, - Paragraph().withChildren( - Span(text = "Have a question? Please ask at "), - Link( - key = "discussion-board-link", - text = "terminal21's discussion board ", - href = "https://github.com/kostaskougios/terminal21-restapi/discussions", - color = Some("teal.500"), - isExternal = Some(true) - ).withChildren(ExternalLinkIcon(mx = Some("2px"))) + val startApp = appsMv.map(_.model).find(_.nonEmpty).flatten + MV( + model.copy(startApp = startApp), + Box().withChildren( + Header1(text = "Terminal 21 Manager"), + Paragraph( + text = """ + |Here you can run all the installed apps on the server.""".stripMargin + ), + appsTable, + Paragraph().withChildren( + Span(text = "Have a question? Please ask at "), + Link( + key = "discussion-board-link", + text = "terminal21's discussion board ", + href = "https://github.com/kostaskougios/terminal21-restapi/discussions", + color = Some("teal.500"), + isExternal = Some(true) + ).withChildren(ExternalLinkIcon(mx = Some("2px"))) + ) ) ) def controller: Controller[ManagerModel] = Controller(components) - .onEvent: event => - import event.* - // for every event, reset the startApp so that it doesn't start the same app on each event - handled.withModel(model.copy(startApp = None)) def eventsIterator: Iterator[ManagerModel] = controller - .render() - .handledEventsIterator + .render(ManagerModel()) + .iterator .map(_.model) .tapEach: m => for app <- m.startApp do startApp(app) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 4588fbd9..86903769 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -25,8 +25,7 @@ class ServerStatusPage( sessionsService: ServerSessionsService )(using appSession: ConnectedSession, fiberExecutor: FiberExecutor): case class StatusModel(runtime: Runtime, sessions: Seq[Session]) - val initModel = StatusModel(Runtime.getRuntime, sessionsService.allSessions) - given Model[StatusModel] = Model(initModel) + val initModel = StatusModel(Runtime.getRuntime, sessionsService.allSessions) case class Ticker(sessions: Seq[Session]) extends ClientEvent @@ -36,38 +35,38 @@ class ServerStatusPage( Thread.sleep(2000) appSession.fireEvents(Ticker(sessionsService.allSessions)) - try controller.render().handledEventsIterator.lastOption + try controller.render(initModel).iterator.lastOption catch case t: Throwable => t.printStackTrace() private def toMb(v: Long) = s"${v / (1024 * 1024)} MB" private val xs = Some("2xs") def controller: Controller[StatusModel] = - Controller(components).onEvent: - case ControllerClientEvent(handled, Ticker(sessions)) => - handled.withModel(handled.model.copy(sessions = sessions)) + Controller(components) - def components: Seq[UiElement] = - Seq(jvmTable, sessionsTable) + def components(model: StatusModel, events: Events): MV[StatusModel] = + MV( + model, + Box().withChildren( + jvmTable(model.runtime, events), + sessionsTable(model.sessions, events) + ) + ) private val jvmTableE = QuickTable(key = "jvmTable", caption = Some("JVM")) .withHeaders("Property", "Value", "Actions") private val gcButton = Button(key = "gc-button", size = xs, text = "Run GC") - .onClick: event => - System.gc() - event.handled - - def jvmTable: UiElement = - jvmTableE.onModelChangeRender: (table, m) => - val runtime = m.runtime - table.withRows( - Seq( - Seq("Free Memory", toMb(runtime.freeMemory()), ""), - Seq("Max Memory", toMb(runtime.maxMemory()), ""), - Seq("Total Memory", toMb(runtime.totalMemory()), gcButton), - Seq("Available processors", runtime.availableProcessors(), "") - ) + + def jvmTable(runtime: Runtime, events: Events): UiElement = + if events.isClicked(gcButton) then System.gc() + jvmTableE.withRows( + Seq( + Seq("Free Memory", toMb(runtime.freeMemory()), ""), + Seq("Max Memory", toMb(runtime.maxMemory()), ""), + Seq("Total Memory", toMb(runtime.totalMemory()), gcButton), + Seq("Available processors", runtime.availableProcessors(), "") ) + ) private val sessionsTableE = QuickTable( @@ -75,33 +74,29 @@ class ServerStatusPage( caption = Some("All sessions") ).withHeaders("Id", "Name", "Is Open", "Actions") - def sessionsTable: UiElement = - sessionsTableE.onModelChangeRender: (table, m) => - val sessions = m.sessions - table.withRows( - sessions.map: session => - Seq(Text(text = session.id), Text(text = session.name), if session.isOpen then CheckIcon() else NotAllowedIcon(), actionsFor(session)) - ) + def sessionsTable(sessions: Seq[Session], events: Events): UiElement = + sessionsTableE.withRows( + sessions.map: session => + Seq(Text(text = session.id), Text(text = session.name), if session.isOpen then CheckIcon() else NotAllowedIcon(), actionsFor(session, events)) + ) - private def actionsFor(session: Session): UiElement = + private def actionsFor(session: Session, events: Events): UiElement = if session.isOpen then + val closeButton = Button(key = s"close-${session.id}", text = "Close", size = xs) + .withLeftIcon(SmallCloseIcon()) + if events.isClicked(closeButton) then sessionsService.terminateAndRemove(session) + val viewButton = Button(key = s"view-${session.id}", text = "View State", size = xs) + .withLeftIcon(ChatIcon()) + if events.isClicked(viewButton) then + serverSideSessions + .withNewSession(session.id + "-server-state", s"Server State:${session.id}") + .connect: sSession => + new ViewServerStatePage(using sSession).runFor(sessionsService.sessionStateOf(session)) + Box().withChildren( - Button(key = s"close-${session.id}", text = "Close", size = xs) - .withLeftIcon(SmallCloseIcon()) - .onClick: event => - import event.* - sessionsService.terminateAndRemove(session) - handled - , + closeButton, Text(text = " "), - Button(key = s"view-${session.id}", text = "View State", size = xs) - .withLeftIcon(ChatIcon()) - .onClick: event => - serverSideSessions - .withNewSession(session.id + "-server-state", s"Server State:${session.id}") - .connect: sSession => - new ViewServerStatePage(using sSession).runFor(sessionsService.sessionStateOf(session)) - event.handled + viewButton ) else NotAllowedIcon() @@ -136,5 +131,5 @@ class ViewServerStatePage(using session: ConnectedSession): keyTreePanel ) ) - Controller.noModel(components).render() + session.render(components) session.leaveSessionOpenAfterExiting() diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala index 370870f3..124022c3 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala @@ -19,11 +19,10 @@ class SettingsApp extends ServerSideApp: new SettingsPage().run() class SettingsPage(using session: ConnectedSession): - import Model.Standard.unitModel val themeToggle = ThemeToggle() def run() = - controller.render().handledEventsIterator.lastOption + controller.render(()).iterator.lastOption def components = Seq(themeToggle) - def controller = Controller(components) + def controller = Controller.noModel(components) diff --git a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala index a46e6a49..70ea67bb 100644 --- a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala +++ b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala @@ -1,63 +1,63 @@ -package org.terminal21.serverapp.bundled - -import org.mockito.Mockito -import org.mockito.Mockito.when -import org.scalatest.funsuite.AnyFunSuiteLike -import org.scalatestplus.mockito.MockitoSugar.mock -import org.terminal21.client.components.* -import org.terminal21.client.components.chakra.{Link, Text} -import org.terminal21.client.{ConnectedSession, ConnectedSessionMock} -import org.terminal21.serverapp.ServerSideApp -import org.scalatest.matchers.should.Matchers.* -import org.terminal21.model.CommandEvent - -class AppManagerPageTest extends AnyFunSuiteLike: - def mockApp(name: String, description: String) = - val app = mock[ServerSideApp] - when(app.name).thenReturn(name) - when(app.description).thenReturn(description) - app - - class App(apps: ServerSideApp*): - given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock - var startedApp: Option[ServerSideApp] = None - val page = new AppManagerPage(apps, app => startedApp = Some(app)) - val model = page.ManagerModel() - def allComponents = page.components.flatMap(_.flat) - - test("renders app links"): - new App(mockApp("app1", "the-app1-desc")): - allComponents - .collect: - case l: Link if l.text == "app1" => l - .size should be(1) - - test("renders app description"): - new App(mockApp("app1", "the-app1-desc")): - allComponents - .collect: - case t: Text if t.text == "the-app1-desc" => t - .size should be(1) - - test("renders the discussions link"): - new App(): - allComponents - .collect: - case l: Link if l.href == "https://github.com/kostaskougios/terminal21-restapi/discussions" => l - .size should be(1) - - test("starts app when app link is clicked"): - val app = mockApp("app1", "the-app1-desc") - new App(app): - val eventsIt = page.eventsIterator - session.fireEvents(CommandEvent.onClick(page.appRows.head.head), CommandEvent.sessionClosed) - eventsIt.toList - startedApp should be(Some(app)) - - test("resets startApp state on other events"): - val app = mockApp("app1", "the-app1-desc") - new App(app): - val other = allComponents.find(_.key == "discussion-board-link").get - val eventsIt = page.controller.render().handledEventsIterator - session.fireEvents(CommandEvent.onClick(page.appRows.head.head), CommandEvent.onClick(other), CommandEvent.sessionClosed) - eventsIt.toList.map(_.model).last.startApp should be(None) +//package org.terminal21.serverapp.bundled +// +//import org.mockito.Mockito +//import org.mockito.Mockito.when +//import org.scalatest.funsuite.AnyFunSuiteLike +//import org.scalatestplus.mockito.MockitoSugar.mock +//import org.terminal21.client.components.* +//import org.terminal21.client.components.chakra.{Link, Text} +//import org.terminal21.client.{ConnectedSession, ConnectedSessionMock} +//import org.terminal21.serverapp.ServerSideApp +//import org.scalatest.matchers.should.Matchers.* +//import org.terminal21.model.CommandEvent +// +//class AppManagerPageTest extends AnyFunSuiteLike: +// def mockApp(name: String, description: String) = +// val app = mock[ServerSideApp] +// when(app.name).thenReturn(name) +// when(app.description).thenReturn(description) +// app +// +// class App(apps: ServerSideApp*): +// given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock +// var startedApp: Option[ServerSideApp] = None +// val page = new AppManagerPage(apps, app => startedApp = Some(app)) +// val model = page.ManagerModel() +// def allComponents = page.components.flatMap(_.flat) +// +// test("renders app links"): +// new App(mockApp("app1", "the-app1-desc")): +// allComponents +// .collect: +// case l: Link if l.text == "app1" => l +// .size should be(1) +// +// test("renders app description"): +// new App(mockApp("app1", "the-app1-desc")): +// allComponents +// .collect: +// case t: Text if t.text == "the-app1-desc" => t +// .size should be(1) +// +// test("renders the discussions link"): +// new App(): +// allComponents +// .collect: +// case l: Link if l.href == "https://github.com/kostaskougios/terminal21-restapi/discussions" => l +// .size should be(1) +// +// test("starts app when app link is clicked"): +// val app = mockApp("app1", "the-app1-desc") +// new App(app): +// val eventsIt = page.eventsIterator +// session.fireEvents(CommandEvent.onClick(page.appRows.head.head), CommandEvent.sessionClosed) +// eventsIt.toList +// startedApp should be(Some(app)) +// +// test("resets startApp state on other events"): +// val app = mockApp("app1", "the-app1-desc") +// new App(app): +// val other = allComponents.find(_.key == "discussion-board-link").get +// val eventsIt = page.controller.render().handledEventsIterator +// session.fireEvents(CommandEvent.onClick(page.appRows.head.head), CommandEvent.onClick(other), CommandEvent.sessionClosed) +// eventsIt.toList.map(_.model).last.startApp should be(None) diff --git a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala index 1ab71f71..6a6bd0f3 100644 --- a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala +++ b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala @@ -1,69 +1,69 @@ -package org.terminal21.serverapp.bundled - -import org.mockito.Mockito.when -import org.scalatest.funsuite.AnyFunSuiteLike -import org.scalatest.matchers.should.Matchers.* -import org.scalatestplus.mockito.MockitoSugar.mock -import org.terminal21.client.components.chakra.{Button, CheckIcon, NotAllowedIcon} -import org.terminal21.client.{ConnectedSession, ConnectedSessionMock, given} -import org.terminal21.model.CommonModelBuilders.session -import org.terminal21.model.{CommandEvent, CommonModelBuilders, Session} -import org.terminal21.server.service.ServerSessionsService -import org.terminal21.serverapp.ServerSideSessions - -class ServerStatusPageTest extends AnyFunSuiteLike: - class App: - given connectedSession: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock - val sessionsService = mock[ServerSessionsService] - val serverSideSessions = mock[ServerSideSessions] - val session1 = session(id = "session1") - when(sessionsService.allSessions).thenReturn(Seq(session1)) - val page = new ServerStatusPage(serverSideSessions, sessionsService) - - test("Close button for a session"): - new App: - page.sessionsTable.flat - .collectFirst: - case b: Button if b.text == "Close" => b - .isEmpty should be(false) - - test("View state button for a session"): - new App: - page.sessionsTable.flat - .collectFirst: - case b: Button if b.text == "View State" => b - .isEmpty should be(false) - - test("When session is open, a CheckIcon is displayed"): - new App: - page.sessionsTable.flat - .collectFirst: - case i: CheckIcon => i - .isEmpty should be(false) - - test("When session is closed, a NotAllowedIcon is displayed"): - new App: - import page.given - val table = page.sessionsTable - val m = page.initModel.copy(sessions = Seq(session(isOpen = false))) - table - .fireModelChangeRender(m) - .flat - .collectFirst: - case i: NotAllowedIcon => i - .isEmpty should be(false) - - test("sessions are rendered when Ticker event is fired"): - new App: - val it = page.controller.render().handledEventsIterator - private val sessions2 = Seq(session(id = "s2", name = "session 2")) - private val sessions3 = Seq(session(id = "s3", name = "session 3")) - connectedSession.fireEvents( - page.Ticker(sessions2), - page.Ticker(sessions3), - CommandEvent.sessionClosed - ) - val handledEvents = it.toList - handledEvents.head.model.sessions should be(Seq(session(id = "session1"))) - handledEvents(1).model.sessions should be(sessions2) - handledEvents(2).model.sessions should be(sessions3) +//package org.terminal21.serverapp.bundled +// +//import org.mockito.Mockito.when +//import org.scalatest.funsuite.AnyFunSuiteLike +//import org.scalatest.matchers.should.Matchers.* +//import org.scalatestplus.mockito.MockitoSugar.mock +//import org.terminal21.client.components.chakra.{Button, CheckIcon, NotAllowedIcon} +//import org.terminal21.client.{ConnectedSession, ConnectedSessionMock, given} +//import org.terminal21.model.CommonModelBuilders.session +//import org.terminal21.model.{CommandEvent, CommonModelBuilders, Session} +//import org.terminal21.server.service.ServerSessionsService +//import org.terminal21.serverapp.ServerSideSessions +// +//class ServerStatusPageTest extends AnyFunSuiteLike: +// class App: +// given connectedSession: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock +// val sessionsService = mock[ServerSessionsService] +// val serverSideSessions = mock[ServerSideSessions] +// val session1 = session(id = "session1") +// when(sessionsService.allSessions).thenReturn(Seq(session1)) +// val page = new ServerStatusPage(serverSideSessions, sessionsService) +// +// test("Close button for a session"): +// new App: +// page.sessionsTable.flat +// .collectFirst: +// case b: Button if b.text == "Close" => b +// .isEmpty should be(false) +// +// test("View state button for a session"): +// new App: +// page.sessionsTable.flat +// .collectFirst: +// case b: Button if b.text == "View State" => b +// .isEmpty should be(false) +// +// test("When session is open, a CheckIcon is displayed"): +// new App: +// page.sessionsTable.flat +// .collectFirst: +// case i: CheckIcon => i +// .isEmpty should be(false) +// +// test("When session is closed, a NotAllowedIcon is displayed"): +// new App: +// import page.given +// val table = page.sessionsTable +// val m = page.initModel.copy(sessions = Seq(session(isOpen = false))) +// table +// .fireModelChangeRender(m) +// .flat +// .collectFirst: +// case i: NotAllowedIcon => i +// .isEmpty should be(false) +// +// test("sessions are rendered when Ticker event is fired"): +// new App: +// val it = page.controller.render().handledEventsIterator +// private val sessions2 = Seq(session(id = "s2", name = "session 2")) +// private val sessions3 = Seq(session(id = "s3", name = "session 3")) +// connectedSession.fireEvents( +// page.Ticker(sessions2), +// page.Ticker(sessions3), +// CommandEvent.sessionClosed +// ) +// val handledEvents = it.toList +// handledEvents.head.model.sessions should be(Seq(session(id = "session1"))) +// handledEvents(1).model.sessions should be(sessions2) +// handledEvents(2).model.sessions should be(sessions3) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index 8c12b244..1e47b689 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -93,7 +93,7 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se * @param es * the UiElements to be rendered. */ - private[client] def render(es: Seq[UiElement]): Unit = + def render(es: Seq[UiElement]): Unit = clear() val j = toJson(es) sessionsService.setSessionJsonState(session, j) @@ -105,7 +105,7 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se private[client] def renderChanges(es: Seq[UiElement]): Unit = if !isClosed && es.nonEmpty then val j = toJson(es) - sessionsService.changeSessionJsonState(session, j) + sessionsService.setSessionJsonState(session, j) // TODO:changeSessionJsonState private def toJson(elementsUn: Seq[UiElement]): ServerJson = val elements = elementsUn.map(_.substituteComponents) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 21c976cb..2555d3fc 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -1,292 +1,57 @@ package org.terminal21.client -import org.slf4j.LoggerFactory import org.terminal21.client.collections.EventIterator -import org.terminal21.client.components.OnChangeEventHandler.CanHandleOnChangeEvent -import org.terminal21.client.components.OnClickEventHandler.CanHandleOnClickEvent -import org.terminal21.client.components.UiElement.HasChildren -import org.terminal21.client.components.{OnChangeBooleanEventHandler, OnChangeEventHandler, OnClickEventHandler, UiElement} -import org.terminal21.collections.{TMMap, TypedMap, TypedMapKey} +import org.terminal21.client.components.UiElement +import org.terminal21.client.components.chakra.Box import org.terminal21.model.{ClientEvent, CommandEvent, OnChange, OnClick} -import scala.reflect.{ClassTag, classTag} +type ModelViewMaterialized[M] = (M, Events) => MV[M] -type EventHandler = PartialFunction[ControllerEvent[_], Handled[_]] -type ComponentsByKey = Map[String, UiElement] - -class Controller( +class Controller[M]( eventIteratorFactory: => Iterator[CommandEvent], renderChanges: Seq[UiElement] => Unit, - modelComponents: Seq[UiElement], - initialModelValues: Map[Model[Any], Any], - eventHandlers: Seq[EventHandler] + materializer: ModelViewMaterialized[M] ): - def withModel[M](model: Model[M], value: M): Controller = - new Controller( - eventIteratorFactory, - renderChanges, - modelComponents, - initialModelValues + (model.asInstanceOf[Model[Any]] -> value), - eventHandlers - ) - - private def applyModelTo(components: Seq[UiElement]): Seq[UiElement] = - components.map: c => - initialModelValues.foldLeft(c): - case (e, (m, v)) => - val ne = if e.hasModelChangeRenderHandler(m) then e.fireModelChangeRender(m)(v) else e - ne match - case ch: HasChildren => ch.withChildren(applyModelTo(ch.children)*) - case x => x - - def render()(using session: ConnectedSession): RenderedController = - val elements = applyModelTo(modelComponents) - session.render(elements) - new RenderedController( - eventIteratorFactory, - initialModelValues, - elements, - renderChanges, - eventHandlers :+ renderChangesEventHandler - ) - - private def renderChangesEventHandler: PartialFunction[ControllerEvent[_], Handled[_]] = - case ControllerClientEvent(h, RenderChangesEvent(changes)) => - h.copy(renderedChanges = h.renderedChanges ++ changes) - - def onEvent(handler: EventHandler) = - new Controller( - eventIteratorFactory, - renderChanges, - modelComponents, - initialModelValues, - eventHandlers :+ handler - ) + def render(initialModel: M): RenderedController[M] = + val mv = materializer(initialModel, Events.Empty) + renderChanges(Seq(mv.view)) + new RenderedController(eventIteratorFactory, renderChanges, materializer, mv) object Controller: -// def apply[M](initialModel: Model[M], modelComponents: Seq[UiElement])(using session: ConnectedSession): Controller[M] = -// new Controller(session.eventIterator, session.renderChanges, modelComponents, initialModel, Nil) - def apply[M](rootModel: Model[M], initialValue: M, modelComponents: Seq[UiElement])(using session: ConnectedSession): Controller = - new Controller(session.eventIterator, session.renderChanges, modelComponents, Map(rootModel.asInstanceOf[Model[Any]] -> initialValue), Nil) - def noModel(modelComponents: Seq[UiElement])(using session: ConnectedSession): Controller = - apply(Model.Standard.unitModel, (), modelComponents)(using session) + def apply[M](materializer: ModelViewMaterialized[M])(using session: ConnectedSession): Controller[M] = + new Controller(session.eventIterator, session.renderChanges, materializer) + + def noModel(components: Seq[UiElement])(using session: ConnectedSession) = + apply((Unit, Events) => MV((), Box().withChildren(components*))) -class RenderedController( +class RenderedController[M]( eventIteratorFactory: => Iterator[CommandEvent], - initialModelValues: Map[Model[Any], Any], - initialComponents: Seq[UiElement], renderChanges: Seq[UiElement] => Unit, - eventHandlers: Seq[EventHandler] -): - private val logger = LoggerFactory.getLogger(getClass) - - private def invokeEventHandlers[A](initHandled: Handled[A], componentsByKey: ComponentsByKey, event: CommandEvent): Handled[A] = - eventHandlers - .foldLeft(initHandled): (h, f) => - event match - case OnClick(key) => - val e = ControllerClickEvent(componentsByKey(key), h) - if f.isDefinedAt(e) then f(e).asInstanceOf[Handled[A]] else h - case OnChange(key, value) => - val receivedBy = componentsByKey(key) - val e = receivedBy match - case _: OnChangeEventHandler.CanHandleOnChangeEvent => ControllerChangeEvent(receivedBy, h, value) - case _: OnChangeBooleanEventHandler.CanHandleOnChangeEvent => ControllerChangeBooleanEvent(receivedBy, h, value.toBoolean) - if f.isDefinedAt(e) then f(e).asInstanceOf[Handled[A]] else h - case ce: ClientEvent => - val e = ControllerClientEvent(h, ce) - if f.isDefinedAt(e) then f(e).asInstanceOf[Handled[A]] else h - case x => throw new IllegalStateException(s"Unexpected state $x") - - private def clickHandlersMap[A](allComponents: Seq[UiElement], h: Handled[A]): Map[String, Seq[OnClickEventHandlerFunction[A]]] = - allComponents - .collect: - case e: OnClickEventHandler.CanHandleOnClickEvent if e.dataStore.contains(h.mm.ClickEventHandlerKey) => (e.key, e.dataStore(h.mm.ClickEventHandlerKey)) - .toMap - - private def changeHandlersMap[A](allComponents: Seq[UiElement], h: Handled[A]): Map[String, Seq[OnChangeEventHandlerFunction[A]]] = - allComponents - .collect: - case e: OnChangeEventHandler.CanHandleOnChangeEvent if e.dataStore.contains(h.mm.ChangeEventHandlerKey) => - (e.key, e.dataStore(h.mm.ChangeEventHandlerKey)) - .toMap - - private def changeBooleanHandlersMap[A](allComponents: Seq[UiElement], h: Handled[A]): Map[String, Seq[OnChangeBooleanEventHandlerFunction[A]]] = - allComponents - .collect: - case e: OnChangeBooleanEventHandler.CanHandleOnChangeEvent if e.dataStore.contains(h.mm.ChangeBooleanEventHandlerKey) => - (e.key, e.dataStore(h.mm.ChangeBooleanEventHandlerKey)) - .toMap - - private def invokeComponentEventHandlers[A](h: Handled[A], componentsByKey: ComponentsByKey, event: CommandEvent): Handled[A] = - val allComponents = componentsByKey.values.toList - lazy val clickHandlers = clickHandlersMap(allComponents, h) - lazy val changeHandlers = changeHandlersMap(allComponents, h) - lazy val changeBooleanHandlers = changeBooleanHandlersMap(allComponents, h) - event match - case OnClick(key) if clickHandlers.contains(key) => - val handlers = clickHandlers(key) - val receivedBy = componentsByKey(key) - val handled = handlers.foldLeft(h): (handled, handler) => - handler(ControllerClickEvent(receivedBy, handled)) - handled - case OnChange(key, value) if changeHandlers.contains(key) => - val handlers = changeHandlers(key) - val receivedBy = componentsByKey(key) - val handled = handlers.foldLeft(h): (handled, handler) => - handler(ControllerChangeEvent(receivedBy, handled, value)) - handled - case OnChange(key, value) if changeBooleanHandlers.contains(key) => - val handlers = changeBooleanHandlers(key) - val receivedBy = componentsByKey(key) - val handled = handlers.foldLeft(h): (handled, handler) => - handler(ControllerChangeBooleanEvent(receivedBy, handled, value.toBoolean)) - handled - case ModelChangeEvent(model, newValue) if model == h.mm => - h.withModel(model, newValue) - case _ => h - - private def checkForDuplicatesAndThrow(components: Seq[UiElement]): Unit = - val duplicates = components.groupBy(_.key).filter(_._2.size > 1).keys.toSet - if duplicates.nonEmpty then - val duplicateComponents = components.filter(e => duplicates.contains(e.key)) - throw new IllegalArgumentException(s"Duplicate(s) found: ${duplicates.mkString(", ")}\nDuplicate components:\n${duplicateComponents.mkString("\n")}") - - private def calcComponentsByKeyMap(components: Seq[UiElement]): Map[String, UiElement] = - val flattened = components - .flatMap(_.flat) - checkForDuplicatesAndThrow(flattened) - val all = flattened - .map(c => (c.key, c)) - .toMap - all.withDefault(key => - throw new IllegalArgumentException( - s"Component with key=$key is not available. Here are all available components:\n${all.values.map(_.toSimpleString).mkString("\n")}" - ) - ) - - private def changesToRenderWhenModelChanges[A]( - oldHandled: Handled[A], - newHandled: Handled[A], - componentsByKey: ComponentsByKey - ): (ComponentsByKey, Handled[A]) = - if oldHandled.modelOption == newHandled.modelOption then (componentsByKey, newHandled) - else - val changeFunctions = - for - e <- componentsByKey.values - f <- e.dataStore.get(newHandled.mm.OnModelChangeRenderKey) - yield (e, f) - - val dsEmpty = TypedMap.empty - val changed = changeFunctions - .map: (e, f) => - (e, f(e, newHandled.model)) - .filter: (e, ne) => - e.withDataStore(dsEmpty) != ne.withDataStore(dsEmpty) - .map(_._2) - .toList - ( - componentsByKey ++ calcComponentsByKeyMap(changed), - newHandled.copy(renderedChanges = newHandled.renderedChanges ++ changed) - ) - - private def initialModelsMap: TypedMap = - val m = initialModelValues.map: (k, v) => - (k.ModelKey, v) - new TypedMap(m.asInstanceOf[TMMap]) - - private def availableModels(componentsByKey: ComponentsByKey): Seq[Model[_]] = - (initialModelValues.keys.toList ++ componentsByKey.values.flatMap(_.handledModels).toList).distinct - - def handledEventsIterator: EventIterator[HandledEvent] = - val initCompByKeyMap = calcComponentsByKeyMap(initialComponents) - val initAvailableModels = availableModels(initCompByKeyMap) - val initHandledEvent = HandledEvent(initAvailableModels, initialModelsMap, initCompByKeyMap, false, Nil) - new EventIterator( - eventIteratorFactory - .takeWhile(!_.isSessionClosed) - .scanLeft(initHandledEvent): - case (ohEvent, event) => - try - val nhEvent = ohEvent.models.foldLeft(ohEvent): - case (oldHandledEvent, model) => - val oldHandled = oldHandledEvent.toHandled(model).copy(renderedChanges = Nil) - val handled2 = invokeEventHandlers(oldHandled, oldHandledEvent.componentsByKey, event) - val handled3 = invokeComponentEventHandlers(handled2, oldHandledEvent.componentsByKey, event) - val (componentsByKey, newHandled) = changesToRenderWhenModelChanges(oldHandled, handled3, oldHandledEvent.componentsByKey) - oldHandledEvent.copy( - modelValues = newHandled.modelValues, - componentsByKey = componentsByKey, - shouldTerminate = newHandled.shouldTerminate, - renderedChanges = newHandled.renderedChanges - ) - if nhEvent.renderedChanges.nonEmpty then renderChanges(nhEvent.renderedChanges) - nhEvent - catch - case t: Throwable => - logger.error("an error occurred while iterating events", t) - ohEvent - .flatMap: h => - // trick to make sure we take the last state of the model when shouldTerminate=true - if h.shouldTerminate then Seq(h.copy(shouldTerminate = false), h) else Seq(h) - .takeWhile(!_.shouldTerminate) - ) - -sealed trait ControllerEvent[M]: - def model: M = handled.model - def handled: Handled[M] - -case class ControllerClickEvent[M](clicked: UiElement, handled: Handled[M]) extends ControllerEvent[M] - -case class ControllerChangeEvent[M](changed: UiElement, handled: Handled[M], newValue: String) extends ControllerEvent[M] - -case class ControllerChangeBooleanEvent[M](changed: UiElement, handled: Handled[M], newValue: Boolean) extends ControllerEvent[M] -case class ControllerClientEvent[M](handled: Handled[M], event: ClientEvent) extends ControllerEvent[M] - -case class Handled[M]( - mm: Model[M], - modelValues: TypedMap, - shouldTerminate: Boolean, - renderedChanges: Seq[UiElement] -): - def model: M = modelValues(mm.ModelKey) - def modelOption: Option[M] = modelValues.get(mm.ModelKey) - def withModel(m: M): Handled[M] = copy(modelValues = modelValues + (mm.ModelKey -> m)) - def withModel[A](model: Model[A], newValue: A): Handled[M] = copy(modelValues = modelValues + (model.ModelKey -> newValue)) - def mapModel(f: M => M): Handled[M] = withModel(f(model)) - def terminate: Handled[M] = copy(shouldTerminate = true) - def withShouldTerminate(t: Boolean): Handled[M] = copy(shouldTerminate = t) - -case class HandledEvent( - models: Seq[Model[_]], - modelValues: TypedMap, - componentsByKey: ComponentsByKey, - shouldTerminate: Boolean, - renderedChanges: Seq[UiElement] + materializer: ModelViewMaterialized[M], + initialMv: MV[M] ): - def model[A](model: Model[A]): A = modelOf(model) - def modelOf[A](model: Model[A]): A = modelValues(model.ModelKey) - def toHandled[A](model: Model[A]): Handled[A] = Handled[A](model, modelValues, shouldTerminate, renderedChanges) - -type OnClickEventHandlerFunction[M] = ControllerClickEvent[M] => Handled[M] -type OnChangeEventHandlerFunction[M] = ControllerChangeEvent[M] => Handled[M] -type OnChangeBooleanEventHandlerFunction[M] = ControllerChangeBooleanEvent[M] => Handled[M] - -class Model[M: ClassTag](name: String): - type OnModelChangeFunction = (UiElement, M) => UiElement - object ModelKey extends TypedMapKey[M] - object OnModelChangeRenderKey extends TypedMapKey[OnModelChangeFunction] - object ClickEventHandlerKey extends TypedMapKey[Seq[OnClickEventHandlerFunction[M]]] - object ChangeEventHandlerKey extends TypedMapKey[Seq[OnChangeEventHandlerFunction[M]]] - object ChangeBooleanEventHandlerKey extends TypedMapKey[Seq[OnChangeBooleanEventHandlerFunction[M]]] - override def toString = s"Model($name)" - -object Model: - def apply[M: ClassTag]: Model[M] = new Model[M](classTag[M].runtimeClass.getName) - def apply[M: ClassTag](name: String): Model[M] = new Model[M](name) - object Standard: - val unitModel: Model[Unit] = Model[Unit]("unit") - -case class RenderChangesEvent(changes: Seq[UiElement]) extends ClientEvent -case class ModelChangeEvent[M](model: Model[M], newValue: M) extends ClientEvent + def iterator: EventIterator[MV[M]] = new EventIterator[MV[M]]( + eventIteratorFactory + .takeWhile(_.isSessionClosed) + .scanLeft(initialMv): (mv, e) => + val events = Events(e) + val newMv = materializer(mv.model, events) + renderChanges(Seq(newMv.view)) + newMv + ) + +case class Events(event: CommandEvent): + def isClicked(e: UiElement): Boolean = event match + case OnClick(key) => key == e.key + case _ => false + + def changedValue(e: UiElement): Option[String] = event match + case OnChange(key, value) if key == e.key => Some(value) + case _ => None + +object Events: + case object InitialRender extends ClientEvent + + val Empty = Events(InitialRender) + +case class MV[M](model: M, view: UiElement) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala index 4abaeee9..9388995c 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala @@ -1,29 +1,18 @@ package org.terminal21.client.components -import org.terminal21.client.{Model, OnChangeBooleanEventHandlerFunction, OnChangeEventHandlerFunction, OnClickEventHandlerFunction} - trait EventHandler object OnClickEventHandler: trait CanHandleOnClickEvent: this: UiElement => if key.isEmpty then throw new IllegalStateException(s"clickables must have a stable key. Error occurred on $this") - def onClick[M](model: Model[M])(h: OnClickEventHandlerFunction[M]): This = - val handlers = dataStore.getOrElse(model.ClickEventHandlerKey, Nil) - store(model.ClickEventHandlerKey, handlers :+ h) object OnChangeEventHandler: trait CanHandleOnChangeEvent: this: UiElement => if key.isEmpty then throw new IllegalStateException(s"changeable must have a stable key. Error occurred on $this") - def onChange[M](model: Model[M])(h: OnChangeEventHandlerFunction[M]): This = - val handlers = dataStore.getOrElse(model.ChangeEventHandlerKey, Nil) - store(model.ChangeEventHandlerKey, handlers :+ h) object OnChangeBooleanEventHandler: trait CanHandleOnChangeEvent: this: UiElement => if key.isEmpty then throw new IllegalStateException(s"changeable must have a stable key. Error occurred on $this") - def onChange[M](model: Model[M])(h: OnChangeBooleanEventHandlerFunction[M]): This = - val handlers = dataStore.getOrElse(model.ChangeBooleanEventHandlerKey, Nil) - store(model.ChangeBooleanEventHandlerKey, handlers :+ h) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/Keys.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/Keys.scala index c03346f6..97aa99c1 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/Keys.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/Keys.scala @@ -5,5 +5,4 @@ import org.terminal21.client.components.UiElement.HasChildren import java.util.concurrent.atomic.AtomicInteger object Keys: - private val id = new AtomicInteger(0) - def nextKey: String = "key-" + id.incrementAndGet() + def nextKey: String = "" diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala index 74b73063..d36904a6 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala @@ -1,89 +1,89 @@ -package org.terminal21.client.components - -import functions.fibers.FiberExecutor -import org.terminal21.client.{ConnectedSession, Model, RenderChangesEvent} -import org.terminal21.client.components.UiElement.HasStyle -import org.terminal21.client.components.chakra.* -import org.terminal21.collections.TypedMap - -import java.util.concurrent.atomic.{AtomicBoolean, AtomicReference} - -/** Creates a standard UI for a calculation which may take time. While the calculation runs, the UI is grayed out, including the dataUi component. When the - * calculation completes, it allows for updating the dataUi component. - * @tparam OUT - * the return value of the calculation. - */ -abstract class StdUiCalculation[OUT]( - val key: String, - name: String, - dataUi: UiElement with HasStyle, - val dataStore: TypedMap = TypedMap.empty -)(using session: ConnectedSession, executor: FiberExecutor) - extends Calculation[OUT] - with UiComponent: - private def model = Model.Standard.unitModel - private val running = new AtomicBoolean(false) - private val currentUi = new AtomicReference(dataUi) - - protected def updateUi(dataUi: UiElement & HasStyle) = currentUi.set(dataUi) - - lazy val badge = Badge() - lazy val recalc = Button(text = "Recalculate", size = Some("sm"), leftIcon = Some(RepeatIcon())).onClick(model): event => - import event.* - if running.compareAndSet(false, true) then - try - reCalculate() - finally running.set(false) - handled - - override lazy val rendered: Seq[UiElement] = - val header = Box( - bg = "green", - p = 4, - children = Seq( - HStack(children = Seq(Text(text = name), badge, recalc)) - ) - ) - Seq(header, dataUi) - - override def onError(t: Throwable): Unit = - session.fireEvent( - RenderChangesEvent( - Seq( - badge.withText(s"Error: ${t.getMessage}").withColorScheme(Some("red")), - dataUi, - recalc.withIsDisabled(None) - ) - ) - ) - super.onError(t) - - override protected def whenResultsNotReady(): Unit = - session.fireEvent( - RenderChangesEvent( - Seq( - badge.withText("Calculating").withColorScheme(Some("purple")), - currentUi.get().withStyle(dataUi.style + ("filter" -> "grayscale(100%)")), - recalc.withIsDisabled(Some(true)) - ) - ) - ) - super.whenResultsNotReady() - - override type This = StdUiCalculation[OUT] - - // probably this class needs redesign - override def withKey(key: String): StdUiCalculation[OUT] = ??? - override def withDataStore(ds: TypedMap): StdUiCalculation[OUT] = ??? - - override protected def whenResultsReady(results: OUT): Unit = - val newDataUi = currentUi.get().withStyle(dataUi.style - "filter") - session.fireEvent( - RenderChangesEvent( - Seq( - badge.withText("Ready").withColorScheme(None), - newDataUi, - recalc.withIsDisabled(Some(false)) - ) - ) - ) +//package org.terminal21.client.components +// +//import functions.fibers.FiberExecutor +//import org.terminal21.client.{ConnectedSession, Model, RenderChangesEvent} +//import org.terminal21.client.components.UiElement.HasStyle +//import org.terminal21.client.components.chakra.* +//import org.terminal21.collections.TypedMap +// +//import java.util.concurrent.atomic.{AtomicBoolean, AtomicReference} +// +///** Creates a standard UI for a calculation which may take time. While the calculation runs, the UI is grayed out, including the dataUi component. When the +// * calculation completes, it allows for updating the dataUi component. +// * @tparam OUT +// * the return value of the calculation. +// */ +//abstract class StdUiCalculation[OUT]( +// val key: String, +// name: String, +// dataUi: UiElement with HasStyle, +// val dataStore: TypedMap = TypedMap.empty +//)(using session: ConnectedSession, executor: FiberExecutor) +// extends Calculation[OUT] +// with UiComponent: +// private def model = Model.Standard.unitModel +// private val running = new AtomicBoolean(false) +// private val currentUi = new AtomicReference(dataUi) +// +// protected def updateUi(dataUi: UiElement & HasStyle) = currentUi.set(dataUi) +// +// lazy val badge = Badge() +// lazy val recalc = Button(text = "Recalculate", size = Some("sm"), leftIcon = Some(RepeatIcon())).onClick(model): event => +// import event.* +// if running.compareAndSet(false, true) then +// try +// reCalculate() +// finally running.set(false) +// handled +// +// override lazy val rendered: Seq[UiElement] = +// val header = Box( +// bg = "green", +// p = 4, +// children = Seq( +// HStack(children = Seq(Text(text = name), badge, recalc)) +// ) +// ) +// Seq(header, dataUi) +// +// override def onError(t: Throwable): Unit = +// session.fireEvent( +// RenderChangesEvent( +// Seq( +// badge.withText(s"Error: ${t.getMessage}").withColorScheme(Some("red")), +// dataUi, +// recalc.withIsDisabled(None) +// ) +// ) +// ) +// super.onError(t) +// +// override protected def whenResultsNotReady(): Unit = +// session.fireEvent( +// RenderChangesEvent( +// Seq( +// badge.withText("Calculating").withColorScheme(Some("purple")), +// currentUi.get().withStyle(dataUi.style + ("filter" -> "grayscale(100%)")), +// recalc.withIsDisabled(Some(true)) +// ) +// ) +// ) +// super.whenResultsNotReady() +// +// override type This = StdUiCalculation[OUT] +// +// // probably this class needs redesign +// override def withKey(key: String): StdUiCalculation[OUT] = ??? +// override def withDataStore(ds: TypedMap): StdUiCalculation[OUT] = ??? +// +// override protected def whenResultsReady(results: OUT): Unit = +// val newDataUi = currentUi.get().withStyle(dataUi.style - "filter") +// session.fireEvent( +// RenderChangesEvent( +// Seq( +// badge.withText("Ready").withColorScheme(None), +// newDataUi, +// recalc.withIsDisabled(Some(false)) +// ) +// ) +// ) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala index e108cc32..63f314ad 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala @@ -1,7 +1,6 @@ package org.terminal21.client.components -import org.terminal21.client.Model -import org.terminal21.client.components.UiElement.{HasChildren, UiElementModelsKey} +import org.terminal21.client.components.UiElement.HasChildren import org.terminal21.client.components.chakra.Box import org.terminal21.collections.{TypedMap, TypedMapKey} @@ -16,15 +15,6 @@ abstract class UiElement extends AnyElement: def withDataStore(ds: TypedMap): This def store[V](key: TypedMapKey[V], value: V): This = withDataStore(dataStore + (key -> value)) - /** This handler will be called whenever the model changes. It will also be called with the initial model before the first render() - */ - def onModelChangeRender[M](model: Model[M])(f: (This, M) => This): This = - store(UiElementModelsKey, handledModels :+ model).store(model.OnModelChangeRenderKey, f.asInstanceOf[model.OnModelChangeFunction]).asInstanceOf[This] - def hasModelChangeRenderHandler[M](model: Model[M]): Boolean = dataStore.contains(model.OnModelChangeRenderKey) - def fireModelChangeRender[M](model: Model[M])(m: M) = - dataStore(model.OnModelChangeRenderKey).apply(this, m) - def handledModels: Seq[Model[_]] = dataStore.get(UiElementModelsKey).toSeq.flatten - /** @return * this element along all it's children flattened */ @@ -39,8 +29,6 @@ abstract class UiElement extends AnyElement: def toSimpleString: String = s"${getClass.getSimpleName}($key)" object UiElement: - object UiElementModelsKey extends TypedMapKey[Seq[Model[_]]] - trait HasChildren: this: UiElement => def children: Seq[UiElement] diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index f48c7fdf..32ee9499 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -14,346 +14,26 @@ import org.terminal21.model.{ClientEvent, CommandEvent, OnChange, OnClick} import java.util.concurrent.atomic.AtomicBoolean class ControllerTest extends AnyFunSuiteLike: - val button = Button() + val button = Button("b1") val buttonClick = OnClick(button.key) - val input = Input() + val input = Input("i1") val inputChange = OnChange(input.key, "new-value") - val checkbox = Checkbox() + val checkbox = Checkbox("c1") val checkBoxChange = OnChange(checkbox.key, "true") given ConnectedSession = ConnectedSessionMock.newConnectedSessionMock - val intModel: Model[Int] = Model[Int]("int-model") - val stringModel: Model[String] = Model[String]("string-model") - def newController[M]( - initialModel: Model[M], - initialValue: M, - events: => Seq[CommandEvent], - modelComponents: Seq[UiElement], + events: Seq[CommandEvent], + materializer: ModelViewMaterialized[M], renderChanges: Seq[UiElement] => Unit = _ => () - ): Controller = + ): Controller[M] = val seList = SEList[CommandEvent]() val it = seList.iterator events.foreach(e => seList.add(e)) seList.add(CommandEvent.sessionClosed) - new Controller(it, renderChanges, modelComponents, Map.empty, Nil).withModel(initialModel, initialValue) - - test("will throw an exception if there is a duplicate key"): - an[IllegalArgumentException] should be thrownBy - newController(Model[Int], 0, Seq(buttonClick), Seq(button, button)).render().handledEventsIterator - - test("onEvent is called"): - newController(intModel, 0, Seq(buttonClick), Seq(button)) - .onEvent: - case ControllerClickEvent[Int @unchecked](_, handled) => - if handled.model > 1 then handled.terminate else handled.withModel(handled.model + 1) - .render() - .handledEventsIterator - .map(_.model(intModel)) - .toList should be(List(0, 1)) - - test("onEvent is called for change"): - newController(intModel, 0, Seq(inputChange), Seq(input)) - .onEvent: - case ControllerChangeEvent[Int @unchecked](_, handled, newValue) => - if handled.model > 1 then handled.terminate else handled.withModel(handled.model + 1) - .render() - .handledEventsIterator - .map(_.model(intModel)) - .toList should be(List(0, 1)) - - test("onEvent not matched for change"): - newController(intModel, 0, Seq(inputChange), Seq(input)) - .onEvent: - case event: ControllerClickEvent[Int @unchecked] => - import event.* - handled.withModel(5) - .render() - .handledEventsIterator - .map(_.model(intModel)) - .toList should be(List(0, 0)) - - test("onEvent is called for change/boolean"): - newController(intModel, 0, Seq(checkBoxChange), Seq(checkbox)) - .onEvent: - case event: ControllerChangeBooleanEvent[Int @unchecked] => - import event.* - if event.model > 1 then handled.terminate else handled.withModel(event.model + 1) - .render() - .handledEventsIterator - .map(_.model(intModel)) - .toList should be(List(0, 1)) - - test("onEvent not matches for change/boolean"): - newController(intModel, 0, Seq(checkBoxChange), Seq(checkbox)) - .onEvent: - case event: ControllerClickEvent[Int @unchecked] => - import event.* - handled.withModel(5) - .render() - .handledEventsIterator - .map(_.model(intModel)) - .toList should be(List(0, 0)) - - case class TestClientEvent(i: Int) extends ClientEvent - - test("onEvent is called for ClientEvent"): - newController(intModel, 0, Seq(TestClientEvent(5)), Seq(button)) - .onEvent: - case ControllerClientEvent[Int @unchecked](handled, event: TestClientEvent) => - handled.withModel(event.i).terminate - .render() - .handledEventsIterator - .map(_.model(intModel)) - .toList should be(List(0, 5)) - - test("onEvent when no partial function matches ClientEvent"): - newController(intModel, 0, Seq(TestClientEvent(5)), Seq(button)) - .onEvent: - case ControllerClickEvent[Int @unchecked](`checkbox`, handled) => - handled.withModel(5).terminate - .render() - .handledEventsIterator - .map(_.model(intModel)) - .toList should be(List(0, 0)) - - test("onClick is called"): - newController( - intModel, - 0, - Seq(buttonClick), - Seq( - button.onClick(intModel): event => - event.handled.withModel(100).terminate - ) - ).render() - .handledEventsIterator - .map(_.model(intModel)) - .toList should be(List(0, 100)) - - test("onClick is called for multiple models"): - newController( - intModel, - 0, - Seq(buttonClick), - Seq( - button - .onClick(intModel): event => - event.handled.withModel(100).terminate - .onClick(stringModel): event => - event.handled.withModel("new").terminate - ) - ).withModel(stringModel, "old") - .render() - .handledEventsIterator - .map(h => (h.model(intModel), h.model(stringModel))) - .toList should be(List((0, "old"), (100, "new"))) - - test("onChange is called"): - newController( - intModel, - 0, - Seq(inputChange), - Seq( - input.onChange(intModel): event => - event.handled.withModel(100).terminate - ) - ).render() - .handledEventsIterator - .map(_.model(intModel)) - .toList should be(List(0, 100)) - - test("onChange/boolean is called"): - newController( - intModel, - 0, - Seq(checkBoxChange), - Seq( - checkbox.onChange(intModel): event => - event.handled.withModel(100).terminate - ) - ).render() - .handledEventsIterator - .map(_.model(intModel)) - .toList should be(List(0, 100)) - - test("terminate is obeyed and latest model state is iterated"): - newController(intModel, 0, Seq(buttonClick, buttonClick, buttonClick), Seq(button)) - .onEvent: - case event: ControllerEvent[Int @unchecked] => - if event.handled.model > 1 then event.handled.terminate.withModel(100) else event.handled.withModel(event.handled.model + 1) - .render() - .handledEventsIterator - .map(_.model(intModel)) - .toList should be(List(0, 1, 2, 100)) - - test("changes are rendered"): - var rendered = Seq.empty[UiElement] - def renderer(s: Seq[UiElement]): Unit = rendered = s - val but = button.onModelChangeRender(intModel): (b, m) => - b.withText(s"changed $m") - - val handled = newController(intModel, 0, Seq(buttonClick), Seq(but), renderer) - .onEvent: - case event: ControllerEvent[Int @unchecked] => - event.handled.withModel(event.model + 1).terminate - .render() - .handledEventsIterator - .toList - - val expected = Seq(but.withText("changed 1")) - rendered should be(expected) - handled.map(_.renderedChanges)(1) should be(expected) - - test("rendered are cleared"): - val but = button.onModelChangeRender(intModel): (b, m) => - if m == 1 then b.withText(s"changed $m") else b - - val handled = newController(intModel, 0, Seq(buttonClick, checkBoxChange), Seq(but, checkbox)) - .onEvent: - case event: ControllerEvent[Int @unchecked] => - val h = event.handled.withModel(event.model + 1) - if h.model > 1 then h.terminate else h - .render() - .handledEventsIterator - .toList - - val rendered = handled.map(_.renderedChanges) - rendered.head should be(Nil) - rendered(1) should be(Seq(but.withText("changed 1"))) - rendered(2) should be(Nil) - - test("components handle events"): - val table = QuickTable().withRows( - Seq( - Seq( - button.onClick(intModel): event => - import event.* - handled.withModel(model + 1).terminate - ) - ) - ) - val handledEvents = newController(intModel, 0, Seq(buttonClick), Seq(table)) - .render() - .handledEventsIterator - .toList - - handledEvents.map(_.model(intModel)) should be(List(0, 1)) - - test("components receive onModelChange"): - val called = new AtomicBoolean(false) - val table = QuickTable() - .withRows( - Seq( - Seq( - button.onClick(intModel): event => - import event.* - handled.withModel(model + 1).terminate - ) - ) - ) - .onModelChangeRender(intModel): (table, _) => - called.set(true) - table - newController(intModel, 0, Seq(buttonClick), Seq(table)) - .render() - .handledEventsIterator - .lastOption - - called.get() should be(true) - - test("applies initial model before rendering"): - val b = button.onModelChangeRender(intModel): (b, m) => - b.withText(s"model $m") - - val connectedSession = mock[ConnectedSession] - newController(intModel, 5, Nil, Seq(b)) - .render()(using connectedSession) - - verify(connectedSession).render(Seq(b.withText("model 5"))) - - test("applies multiple initial model before rendering"): - val b = button - .onModelChangeRender(intModel): (b, m) => - b.withText(s"model $m") - .onModelChangeRender(stringModel): (b, m) => - b.withText(b.text + s" model $m") - - val connectedSession = mock[ConnectedSession] - newController(intModel, 5, Nil, Seq(b)) - .withModel(stringModel, "x") - .render()(using connectedSession) - - verify(connectedSession).render(Seq(b.withText("model 5 model x"))) - - test("RenderChangesEvent renders changes"): - val handledEvents = newController(intModel, 5, Seq(RenderChangesEvent(Seq(button.withText("changed")))), Seq(button)) - .render() - .handledEventsIterator - .toList - - handledEvents(1).renderedChanges should be(Seq(button.withText("changed"))) - - test("ModelChangeEvent"): - val handledEvents = newController(stringModel, "v", Seq(ModelChangeEvent(intModel, 6)), Nil).withModel(intModel, 5).render().handledEventsIterator.toList - handledEvents(1).modelOf(intModel) should be(6) - - test("onModelChange for different model"): - val b1 = button.onModelChangeRender(intModel): (b, m) => - b.withText(s"changed $m") - - val handledEvents = newController(stringModel, "v", Seq(ModelChangeEvent(intModel, 6)), Seq(b1)).render().handledEventsIterator.toList - - handledEvents(1).renderedChanges should be(Seq(b1.withText("changed 6"))) - - test("onModelChange but no change to element"): - val b1 = button.onModelChangeRender(intModel): (b, _) => - b - - val handledEvents = newController(stringModel, "v", Seq(ModelChangeEvent(intModel, 6)), Seq(b1)).render().handledEventsIterator.toList - - handledEvents(1).renderedChanges should be(Nil) - - test("onModelChange when model change triggered by event"): - val b1 = Button().onModelChangeRender(intModel): (b, m) => - b.withText(s"changed $m") - val b2 = button.onClick(intModel): event => - event.handled.mapModel(_ + 1) - val handledEvents = newController(intModel, 5, Seq(buttonClick), Seq(b1, b2)).render().handledEventsIterator.toList - - handledEvents(1).renderedChanges should be(Seq(b1.withText("changed 6"))) - - test("onModelChange hierarchy"): - val b1 = Button() - .onModelChangeRender(intModel): (b, m) => - b.withText(s"changed $m") - .onClick(stringModel): event => // does nothing, just simulates that this button is actually for a different model - event.handled - val b2 = button.onClick(intModel): event => - event.handled.mapModel(_ + 1) - val box = Box().withChildren(b1, Paragraph().withChildren(b2)) - - val handledEvents = newController(intModel, 5, Seq(buttonClick), Seq(box)).render().handledEventsIterator.toList - - handledEvents(1).renderedChanges should be(Seq(b1.withText("changed 6"))) - - test("onModelChange hierarchy with component"): - val t1 = QuickTable() - .onModelChangeRender(intModel): (table, m) => - table.withRows(Seq(Seq(s"changed $m"))) - val b2 = button.onClick(intModel): event => - event.handled.mapModel(_ + 1) - val box = Box().withChildren(t1, Paragraph().withChildren(b2)) - - val handledEvents = newController(intModel, 5, Seq(buttonClick), Seq(box)).render().handledEventsIterator.toList - - handledEvents(1).renderedChanges should be(Seq(t1.withRows(Seq(Seq(s"changed 6"))))) - - test("onChildModelChange"): - case class Events(key: String): - def changedValue(e: UiElement): Option[String] = ??? - case class MV[M](model: M, view: UiElement) + new Controller(it, renderChanges, materializer) + test("poc"): case class Person(id: Int, name: String) def personComponent(person: Person, events: Events): MV[Person] = val nameInput = Input(s"person-${person.id}", defaultValue = person.name) @@ -376,4 +56,8 @@ class ControllerTest extends AnyFunSuiteLike: MV(peopleComponents.map(_.model), component) val people = Seq(Person(10, "person 1"), Person(20, "person 2")) - val pc = peopleComponent(people, Events("x")) + val all = newController(Seq(OnChange("person-10", "changed p10")), peopleComponent) + .render(people) + .iterator + .toList + println(all.mkString("\n")) From 3dfe26e87e6a6cc533598619f89c8f60bc985bab Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 4 Mar 2024 21:34:29 +0000 Subject: [PATCH 261/313] - --- .../serverapp/bundled/AppManager.scala | 11 +-- .../serverapp/bundled/ServerStatusApp.scala | 24 +----- .../service/ServerSessionsService.scala | 11 +-- .../service/ServerSessionsServiceTest.scala | 58 ++++++++------- .../org/terminal21/ui/std/ServerJson.scala | 42 +---------- .../terminal21/ui/std/ServerJsonTest.scala | 29 -------- .../ui/std/StdExportsBuilders.scala | 10 +-- .../terminal21/client/ConnectedSession.scala | 23 +----- .../client/ConnectedSessionTest.scala | 74 ++++++++++--------- 9 files changed, 89 insertions(+), 193 deletions(-) delete mode 100644 terminal21-ui-std-exports/src/test/scala/org/terminal21/ui/std/ServerJsonTest.scala diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala index da1ca899..45bb415f 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala @@ -29,11 +29,12 @@ class AppManagerPage(apps: Seq[ServerSideApp], startApp: ServerSideApp => Unit)( def run(): Unit = eventsIterator.foreach(_ => ()) - def appRows(events: Events): Seq[MV[Option[ServerSideApp]]] = apps.map: app => + private case class TableView(clicked: Option[ServerSideApp], columns: Seq[UiElement]) + private def appRows(events: Events): Seq[TableView] = apps.map: app => val link = Link(key = s"app-${app.name}", text = app.name) - MV( + TableView( if events.isClicked(link) then Some(app) else None, - Box().withChildren( + Seq( link, Text(text = app.description) ) @@ -44,9 +45,9 @@ class AppManagerPage(apps: Seq[ServerSideApp], startApp: ServerSideApp => Unit)( val appsTable = QuickTable( key = "apps-table", caption = Some("Apps installed on the server, click one to run it."), - rows = appsMv.map(m => Seq(m.view)) + rows = appsMv.map(tv => tv.columns) ).withHeaders("App Name", "Description") - val startApp = appsMv.map(_.model).find(_.nonEmpty).flatten + val startApp = appsMv.map(_.clicked).find(_.nonEmpty).flatten MV( model.copy(startApp = startApp), Box().withChildren( diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 86903769..2c0de8ff 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -4,6 +4,7 @@ import functions.fibers.FiberExecutor import org.terminal21.client.* import org.terminal21.client.components.* import org.terminal21.client.components.chakra.* +import org.terminal21.client.components.std.Paragraph import org.terminal21.model.{ClientEvent, Session} import org.terminal21.server.Dependencies import org.terminal21.server.model.SessionState @@ -105,30 +106,11 @@ class ViewServerStatePage(using session: ConnectedSession): def runFor(state: SessionState): Unit = val sj = state.serverJson - val rootKeyPanel = Seq( - QuickTable() - .withCaption("Root Keys") - .withHeaders("Root Key") - .withRows( - sj.rootKeys.sorted.map(k => Seq(k)) - ) - ) - - val keyTreePanel = Seq( - QuickTable() - .withCaption("Key Tree") - .withHeaders("Key", "Component Json", "Children") - .withRows( - sj.keyTree.toSeq.sortBy(_._1).map((k, v) => Seq(k, sj.elements(k).noSpaces, v.mkString(", "))) - ) - ) - val components = Seq( QuickTabs() - .withTabs("Root Keys", "Key Tree") + .withTabs("Json") .withTabPanels( - rootKeyPanel, - keyTreePanel + Seq(Paragraph(text = sj.toString)) ) ) session.render(components) diff --git a/terminal21-server/src/main/scala/org/terminal21/server/service/ServerSessionsService.scala b/terminal21-server/src/main/scala/org/terminal21/server/service/ServerSessionsService.scala index 18c101a3..f17e6137 100644 --- a/terminal21-server/src/main/scala/org/terminal21/server/service/ServerSessionsService.scala +++ b/terminal21-server/src/main/scala/org/terminal21/server/service/ServerSessionsService.scala @@ -65,11 +65,12 @@ class ServerSessionsService extends SessionsService: logger.debug(s"Session $session new state $newStateJson") override def changeSessionJsonState(session: Session, change: ServerJson): Unit = - val oldV = sessions(session) - val newV = oldV.withNewState(oldV.serverJson.include(change)) - sessions += session -> newV - sessionStateChangeNotificationRegistry.notifyAll((session, newV, Some(change))) - logger.debug(s"Session $session change $change") + ??? +// val oldV = sessions(session) +// val newV = oldV.withNewState(oldV.serverJson.include(change)) +// sessions += session -> newV +// sessionStateChangeNotificationRegistry.notifyAll((session, newV, Some(change))) +// logger.debug(s"Session $session change $change") def triggerUiEvent(event: UiEvent): Unit = val e = event match diff --git a/terminal21-server/src/test/scala/org/terminal21/server/service/ServerSessionsServiceTest.scala b/terminal21-server/src/test/scala/org/terminal21/server/service/ServerSessionsServiceTest.scala index fff56a71..e574b852 100644 --- a/terminal21-server/src/test/scala/org/terminal21/server/service/ServerSessionsServiceTest.scala +++ b/terminal21-server/src/test/scala/org/terminal21/server/service/ServerSessionsServiceTest.scala @@ -105,36 +105,38 @@ class ServerSessionsServiceTest extends AnyFunSuiteLike: listenerCalled should be(2) test("changeSessionJsonState changes session's state"): - new App: - val session = createSession() - val sj1 = serverJson(elements = Map("e1" -> Json.fromString("e1v"))) - serverSessionsService.setSessionJsonState(session, sj1) - val sj2 = serverJson(elements = Map("e2" -> Json.fromString("e2v"))) - serverSessionsService.changeSessionJsonState(session, sj2) - serverSessionsService.sessionStateOf(session).serverJson should be( - sj1.include(sj2) - ) + ??? +// new App: +// val session = createSession() +// val sj1 = serverJson(elements = Map("e1" -> Json.fromString("e1v"))) +// serverSessionsService.setSessionJsonState(session, sj1) +// val sj2 = serverJson(elements = Map("e2" -> Json.fromString("e2v"))) +// serverSessionsService.changeSessionJsonState(session, sj2) +// serverSessionsService.sessionStateOf(session).serverJson should be( +// sj1.include(sj2) +// ) test("changeSessionJsonState notifies listeners"): - new App: - val session = createSession() - val sj1 = serverJson(elements = Map("e1" -> Json.fromString("e1v"))) - serverSessionsService.setSessionJsonState(session, sj1) - val sj2 = serverJson(elements = Map("e2" -> Json.fromString("e2v"))) - var called = 0 - serverSessionsService.notifyMeWhenSessionChanges: (s, sessionState, sjOption) => - called match - case 0 => - s should be(session) - sjOption should be(None) - case 1 => - s should be(session) - sjOption should be(Some(sj2)) - - called += 1 - true - serverSessionsService.changeSessionJsonState(session, sj2) - called should be(2) + ??? +// new App: +// val session = createSession() +// val sj1 = serverJson(elements = Map("e1" -> Json.fromString("e1v"))) +// serverSessionsService.setSessionJsonState(session, sj1) +// val sj2 = serverJson(elements = Map("e2" -> Json.fromString("e2v"))) +// var called = 0 +// serverSessionsService.notifyMeWhenSessionChanges: (s, sessionState, sjOption) => +// called match +// case 0 => +// s should be(session) +// sjOption should be(None) +// case 1 => +// s should be(session) +// sjOption should be(Some(sj2)) +// +// called += 1 +// true +// serverSessionsService.changeSessionJsonState(session, sj2) +// called should be(2) test("triggerUiEvent notifies listeners for clicks"): new App: diff --git a/terminal21-ui-std-exports/src/main/scala/org/terminal21/ui/std/ServerJson.scala b/terminal21-ui-std-exports/src/main/scala/org/terminal21/ui/std/ServerJson.scala index 661e56fb..2c672740 100644 --- a/terminal21-ui-std-exports/src/main/scala/org/terminal21/ui/std/ServerJson.scala +++ b/terminal21-ui-std-exports/src/main/scala/org/terminal21/ui/std/ServerJson.scala @@ -4,44 +4,8 @@ import io.circe.Json import org.slf4j.LoggerFactory case class ServerJson( - rootKeys: Seq[String], - elements: Map[String, Json], - keyTree: Map[String, Seq[String]] -): - private def allChildren(k: String): Seq[String] = - val ch = keyTree(k) - ch ++ ch.flatMap(allChildren) + elements: Seq[Json] +) - def include(j: ServerJson): ServerJson = - try - val allCurrentChildren = j.rootKeys.flatMap(allChildren) - val sj = ServerJson( - rootKeys, - (elements -- allCurrentChildren) ++ j.elements, - (keyTree -- allCurrentChildren) ++ j.keyTree - ) - sj - catch - case t: Throwable => - LoggerFactory - .getLogger(getClass) - .error( - s""" - |Got an invalid ServerJson that caused an error. - |Before receiving: - |${toHumanReadableString} - |The received: - |${j.toHumanReadableString} - |""".stripMargin, - t - ) - throw t - - def toHumanReadableString: String = - s""" - |Root keys : ${rootKeys.mkString(", ")} - |Element keys : ${elements.keys.mkString(", ")} - |keyTree : ${keyTree.mkString(", ")} - |""".stripMargin object ServerJson: - val Empty = ServerJson(Nil, Map.empty, Map.empty) + val Empty = ServerJson(Nil) diff --git a/terminal21-ui-std-exports/src/test/scala/org/terminal21/ui/std/ServerJsonTest.scala b/terminal21-ui-std-exports/src/test/scala/org/terminal21/ui/std/ServerJsonTest.scala deleted file mode 100644 index bd5eebf7..00000000 --- a/terminal21-ui-std-exports/src/test/scala/org/terminal21/ui/std/ServerJsonTest.scala +++ /dev/null @@ -1,29 +0,0 @@ -package org.terminal21.ui.std - -import io.circe.Json -import org.scalatest.funsuite.AnyFunSuiteLike -import org.scalatest.matchers.should.Matchers.* - -class ServerJsonTest extends AnyFunSuiteLike: - test("include"): - val j1 = ServerJson(Seq("k1"), Map("k1" -> Json.fromInt(1), "k2" -> Json.fromInt(2), "k3" -> Json.fromInt(3)), Map("k1" -> Seq("k2", "k3"), "k2" -> Nil)) - val j2 = ServerJson(Seq("k2"), Map("k2" -> Json.fromInt(3)), Map("k2" -> Seq("k4"))) - j1.include(j2) should be( - ServerJson( - Seq("k1"), - Map("k1" -> Json.fromInt(1), "k2" -> Json.fromInt(3), "k3" -> Json.fromInt(3)), - Map("k1" -> Seq("k2", "k3"), "k2" -> Seq("k4")) - ) - ) - - test("include drops unused keys"): - val j1 = ServerJson( - Seq("root1"), - Map("root1" -> Json.fromInt(1), "k2" -> Json.fromInt(2), "k2-c1" -> Json.fromInt(21), "k2-c1-c1" -> Json.fromInt(211)), - Map("k1" -> Seq("k2"), "k2" -> Seq("k2-c1"), "k2-c1" -> Seq("k2-c1-c1"), "k2-c1-c1" -> Nil) - ) - val j2 = ServerJson(Seq("k2"), Map("k2" -> Json.fromInt(3)), Map("k2" -> Nil)) - val r = j1.include(j2) - r.rootKeys should be(Seq("root1")) - r.keyTree should be(Map("k1" -> Seq("k2"), "k2" -> Nil)) - r.elements should be(Map("root1" -> Json.fromInt(1), "k2" -> Json.fromInt(3))) diff --git a/terminal21-ui-std-exports/src/test/scala/org/terminal21/ui/std/StdExportsBuilders.scala b/terminal21-ui-std-exports/src/test/scala/org/terminal21/ui/std/StdExportsBuilders.scala index 68f693e6..6daa5518 100644 --- a/terminal21-ui-std-exports/src/test/scala/org/terminal21/ui/std/StdExportsBuilders.scala +++ b/terminal21-ui-std-exports/src/test/scala/org/terminal21/ui/std/StdExportsBuilders.scala @@ -4,11 +4,5 @@ import io.circe.Json object StdExportsBuilders: def serverJson( - rootKeys: Seq[String] = Nil, - elements: Map[String, Json] = Map.empty, - keyTree: Map[String, Seq[String]] = Map.empty - ) = ServerJson( - rootKeys, - elements, - keyTree - ) + elements: Seq[Json] = Nil + ) = ServerJson(elements) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index 1e47b689..69152678 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -109,28 +109,7 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se private def toJson(elementsUn: Seq[UiElement]): ServerJson = val elements = elementsUn.map(_.substituteComponents) - val flat = elements.flatMap(_.flat) val sj = ServerJson( - elements.map(_.key), - flat - .map: el => - ( - el.key, - el match - case e: UiComponent => encoding.uiElementEncoder(e).deepDropNullValues - case e: HasChildren => encoding.uiElementEncoder(e.noChildren).deepDropNullValues - case e => encoding.uiElementEncoder(e).deepDropNullValues - ) - .toMap, - flat - .map: e => - ( - e.key, - e match - case e: UiComponent => e.rendered.map(_.key) - case e: HasChildren => e.children.map(_.key) - case _ => Nil - ) - .toMap + elements.map(e => encoding.uiElementEncoder(e).deepDropNullValues) ) sj diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala index bbe3e893..f7369a8c 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala @@ -29,41 +29,43 @@ class ConnectedSessionTest extends AnyFunSuiteLike: ) test("to server json"): - val (sessionService, connectedSession) = ConnectedSessionMock.newConnectedSessionAndSessionServiceMock - - val p1 = Paragraph(key = "pk", text = "p1") - val span1 = Span(key = "sk", text = "span1") - connectedSession.render(Seq(p1.withChildren(span1))) - connectedSession.render(Nil) - verify(sessionService).setSessionJsonState( - connectedSession.session, - ServerJson( - Seq("root"), - Map( - "root" -> encoder(Box("root")).deepDropNullValues, - p1.key -> encoder(p1.withChildren()).deepDropNullValues, - span1.key -> encoder(span1).deepDropNullValues - ), - Map("root" -> List(p1.key), p1.key -> Seq(span1.key), span1.key -> Nil) - ) - ) + ??? +// val (sessionService, connectedSession) = ConnectedSessionMock.newConnectedSessionAndSessionServiceMock +// +// val p1 = Paragraph(key = "pk", text = "p1") +// val span1 = Span(key = "sk", text = "span1") +// connectedSession.render(Seq(p1.withChildren(span1))) +// connectedSession.render(Nil) +// verify(sessionService).setSessionJsonState( +// connectedSession.session, +// ServerJson( +// Seq("root"), +// Map( +// "root" -> encoder(Box("root")).deepDropNullValues, +// p1.key -> encoder(p1.withChildren()).deepDropNullValues, +// span1.key -> encoder(span1).deepDropNullValues +// ), +// Map("root" -> List(p1.key), p1.key -> Seq(span1.key), span1.key -> Nil) +// ) +// ) test("renderChanges changes state on server"): - val (sessionService, connectedSession) = ConnectedSessionMock.newConnectedSessionAndSessionServiceMock - - val p1 = Paragraph(key = "pk", text = "p1") - val span1 = Span(key = "sk", text = "span1") - connectedSession.render(Seq(p1)) - connectedSession.renderChanges(Seq(p1.withChildren(span1))) - verify(sessionService).changeSessionJsonState( - connectedSession.session, - ServerJson( - Seq("root"), - Map( - "root" -> encoder(Box("root")).deepDropNullValues, - p1.key -> encoder(p1.withChildren()).deepDropNullValues, - span1.key -> encoder(span1).deepDropNullValues - ), - Map("root" -> List(p1.key), p1.key -> Seq(span1.key), span1.key -> Nil) - ) - ) + ??? +// val (sessionService, connectedSession) = ConnectedSessionMock.newConnectedSessionAndSessionServiceMock +// +// val p1 = Paragraph(key = "pk", text = "p1") +// val span1 = Span(key = "sk", text = "span1") +// connectedSession.render(Seq(p1)) +// connectedSession.renderChanges(Seq(p1.withChildren(span1))) +// verify(sessionService).changeSessionJsonState( +// connectedSession.session, +// ServerJson( +// Seq("root"), +// Map( +// "root" -> encoder(Box("root")).deepDropNullValues, +// p1.key -> encoder(p1.withChildren()).deepDropNullValues, +// span1.key -> encoder(span1).deepDropNullValues +// ), +// Map("root" -> List(p1.key), p1.key -> Seq(span1.key), span1.key -> Nil) +// ) +// ) From 7b8026001079a7829b3c0e3140adea7324fcb1e9 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 4 Mar 2024 22:09:45 +0000 Subject: [PATCH 262/313] - --- .../serverapp/bundled/ServerStatusApp.scala | 12 ++++++++---- .../scala/org/terminal21/client/Controller.scala | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 2c0de8ff..875c0a52 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -46,11 +46,15 @@ class ServerStatusPage( Controller(components) def components(model: StatusModel, events: Events): MV[StatusModel] = + val newModel = events.event match + case Ticker(sessions) => model.copy(sessions = sessions) + case _ => model + MV( - model, + newModel, Box().withChildren( - jvmTable(model.runtime, events), - sessionsTable(model.sessions, events) + jvmTable(newModel.runtime, events), + sessionsTable(newModel.sessions, events) ) ) @@ -110,7 +114,7 @@ class ViewServerStatePage(using session: ConnectedSession): QuickTabs() .withTabs("Json") .withTabPanels( - Seq(Paragraph(text = sj.toString)) + Seq(Paragraph(text = sj.elements.toString)) ) ) session.render(components) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 2555d3fc..89a35d50 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -32,7 +32,7 @@ class RenderedController[M]( ): def iterator: EventIterator[MV[M]] = new EventIterator[MV[M]]( eventIteratorFactory - .takeWhile(_.isSessionClosed) + .takeWhile(!_.isSessionClosed) .scanLeft(initialMv): (mv, e) => val events = Events(e) val newMv = materializer(mv.model, events) From ea893081bb2fc4be79f60cb4745fe3fc74bee04c Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Mon, 4 Mar 2024 22:42:52 +0000 Subject: [PATCH 263/313] - --- .../main/scala/tests/chakra/ChakraModel.scala | 2 - .../src/main/scala/tests/chakra/Forms.scala | 110 ++++++++---------- .../src/main/scala/tests/chakra/Overlay.scala | 38 +++--- .../org/terminal21/client/Controller.scala | 2 + 4 files changed, 70 insertions(+), 82 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/chakra/ChakraModel.scala b/end-to-end-tests/src/main/scala/tests/chakra/ChakraModel.scala index 08ab223b..c96b21a4 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/ChakraModel.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/ChakraModel.scala @@ -2,9 +2,7 @@ package tests.chakra case class ChakraModel( rerun: Boolean = false, - box1: String = "Clicks will be reported here.", editableStatus: String = "This will reflect any changes in the form.", - formStatus: String = "This will reflect any changes in the form.", email: String = "the-test-email@email.com", breadcrumbStatus: String = "no-breadcrumb-clicked", linkStatus: String = "no-link-clicked" diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala b/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala index 1298a78a..1afc5665 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala @@ -6,68 +6,34 @@ import org.terminal21.client.components.chakra.* import tests.chakra.Common.* object Forms: - def components(m: ChakraModel)(using Model[ChakraModel]): Seq[UiElement] = - val status = Box().onModelChangeRender: (b, m) => - b.withText(m.formStatus) + def components(m: ChakraModel, events: Events): Seq[UiElement] = val okIcon = CheckCircleIcon(color = Some("green")) val notOkIcon = WarningTwoIcon(color = Some("red")) - val emailRightAddOn = InputRightAddon().onModelChangeRender: (i, m) => - i.withChildren(if m.email.contains("@") then okIcon else notOkIcon) - - val email = Input(key = "email", `type` = "email", defaultValue = m.email) - .onChange: event => - import event.* - handled.mapModel(_.copy(email = newValue, formStatus = s"email input new value = $newValue")) - + val email = Input(key = "email", `type` = "email", defaultValue = m.email) val description = Textarea(key = "textarea", placeholder = "Please enter a few things about you", defaultValue = "desc") - .onChange: event => - import event.* - handled.mapModel(_.copy(formStatus = s"description input new value = $newValue")) - - val select1 = Select(key = "male/female", placeholder = "Please choose") + val select1 = Select(key = "male/female", placeholder = "Please choose") .withChildren( Option_(text = "Male", value = "male"), Option_(text = "Female", value = "female") ) - .onChange: event => - import event.* - handled.mapModel(_.copy(formStatus = s"select1 input new value = $newValue")) - - val select2 = + val select2 = Select(key = "select-first-second", defaultValue = "1", bg = Some("tomato"), color = Some("black"), borderColor = Some("yellow")).withChildren( Option_(text = "First", value = "1"), Option_(text = "Second", value = "2") ) + val password = Input(key = "password", `type` = "password", defaultValue = "mysecret") + val dob = Input(key = "dob", `type` = "datetime-local") + val color = Input(key = "color", `type` = "color") + val checkbox2 = Checkbox(key = "cb2", text = "Check 2", defaultChecked = true) + val checkbox1 = Checkbox(key = "cb1", text = "Check 1") - val password = Input(key = "password", `type` = "password", defaultValue = "mysecret") - val dob = Input(key = "dob", `type` = "datetime-local") - .onChange: event => - import event.* - handled.mapModel(_.copy(formStatus = s"dob = $newValue")) - - val color = Input(key = "color", `type` = "color") - .onChange: event => - import event.* - handled.mapModel(_.copy(formStatus = s"color = $newValue")) - - val checkbox2 = Checkbox(key = "cb2", text = "Check 2", defaultChecked = true) - .onChange: event => - import event.* - handled.mapModel(_.copy(formStatus = s"checkbox2 checked is $newValue")) - - val checkbox1 = Checkbox(key = "cb1", text = "Check 1") - .onChange: event => - import event.* - handled.mapModel(_.copy(formStatus = s"checkbox1 checked is $newValue")) - - val switch1 = Switch(key = "sw1", text = "Switch 1") - .onChange: event => - import event.* - handled.mapModel(_.copy(formStatus = s"switch1 checked is $newValue")) - val switch2 = Switch(key = "sw2", text = "Switch 2", defaultChecked = true) - - val radioGroup = RadioGroup(key = "radio", defaultValue = "2") + val newM = m.copy( + email = events.changedValue(email).getOrElse(m.email) + ) + val switch1 = Switch(key = "sw1", text = "Switch 1") + val switch2 = Switch(key = "sw2", text = "Switch 2", defaultChecked = true) + val radioGroup = RadioGroup(key = "radio", defaultValue = "2") .withChildren( HStack().withChildren( Radio(value = "1", text = "first"), @@ -75,9 +41,38 @@ object Forms: Radio(value = "3", text = "third") ) ) - .onChange: event => - import event.* - handled.mapModel(_.copy(formStatus = s"radioGroup newValue=$newValue")) + val saveButton = Button(key = "save-button", text = "Save", colorScheme = Some("red")) + val cancelButton = Button(key = "cancel-button", text = "Cancel") + val formStatus = + events + .changedValue(email) + .map(v => s"email input new value = $v") + .toSeq ++ events + .changedValue(description) + .map(v => s"description input new value = $v") ++ events + .changedValue(select1) + .map(v => s"select1 input new value = $v") ++ events + .changedValue(dob) + .map(v => s"dob = $v") ++ events + .changedValue(color) + .map(v => s"color = $v") ++ events + .changedValue(checkbox2) + .map(v => s"checkbox2 checked is $v") ++ events + .changedValue(checkbox1) + .map(v => s"checkbox1 checked is $v") ++ events + .changedValue(switch1) + .map(v => s"switch1 checked is $v") ++ events + .changedValue(radioGroup) + .map(v => s"radioGroup newValue is $v") ++ events + .ifClicked(saveButton, "Saved clicked") ++ events + .ifClicked(cancelButton, "Cancel clicked") + .headOption + .getOrElse("This will reflect any changes in the form.") + + val status = Box(text = formStatus) + + val emailRightAddOn = InputRightAddon() + .withChildren(if newM.email.contains("@") then okIcon else notOkIcon) Seq( commonBox(text = "Forms"), @@ -133,15 +128,8 @@ object Forms: switch2 ), ButtonGroup(variant = Some("outline"), spacing = Some("24")).withChildren( - Button(key = "save-button", text = "Save", colorScheme = Some("red")) - .onClick: event => - import event.* - handled.mapModel(_.copy(formStatus = s"Saved clicked")) - , - Button(key = "cancel-button", text = "Cancel") - .onClick: event => - import event.* - handled.mapModel(_.copy(formStatus = s"Cancel clicked")) + saveButton, + cancelButton ), radioGroup, status diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala b/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala index 883f36d1..2fc72ef9 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala @@ -6,9 +6,21 @@ import org.terminal21.client.components.chakra.* import tests.chakra.Common.commonBox object Overlay: - def components(using Model[ChakraModel]): Seq[UiElement] = - val box1 = Box().onModelChangeRender: (b, m) => - b.withText(m.box1) + def components(events: Events): Seq[UiElement] = + val mi1 = MenuItem(key = "download-menu", text = "Download menu-download") + val mi2 = MenuItem(key = "copy-menu", text = "Copy") + val mi3 = MenuItem(key = "paste-menu", text = "Paste") + val mi4 = MenuItem(key = "exit-menu", text = "Exit") + + val box1Msg = + if events.isClicked(mi1) then "'Download' clicked" + else if events.isClicked(mi2) then "'Copy' clicked" + else if events.isClicked(mi3) then "'Paste' clicked" + else if events.isClicked(mi4) then "'Exit' clicked" + else "Clicks will be reported here." + + val box1 = Box(text = box1Msg) + Seq( commonBox(text = "Menus box0001"), HStack().withChildren( @@ -17,23 +29,11 @@ object Overlay: ChevronDownIcon() ), MenuList().withChildren( - MenuItem(key = "download-menu", text = "Download menu-download") - .onClick: event => - import event.* - handled.mapModel(_.copy(box1 = "'Download' clicked")) - , - MenuItem(key = "copy-menu", text = "Copy").onClick: event => - import event.* - handled.mapModel(_.copy(box1 = "'Copy' clicked")) - , - MenuItem(key = "paste-menu", text = "Paste").onClick: event => - import event.* - handled.mapModel(_.copy(box1 = "'Paste' clicked")) - , + mi1, + mi2, + mi3, MenuDivider(), - MenuItem(key = "exit-menu", text = "Exit").onClick: event => - import event.* - handled.mapModel(_.copy(box1 = "'Exit' clicked")) + mi4 ) ), box1 diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 89a35d50..561de543 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -45,6 +45,8 @@ case class Events(event: CommandEvent): case OnClick(key) => key == e.key case _ => false + def ifClicked[V](e: UiElement, value: => V): Option[V] = if isClicked(e) then Some(value) else None + def changedValue(e: UiElement): Option[String] = event match case OnChange(key, value) if key == e.key => Some(value) case _ => None From 8269b5304ae807c908a4bbfbb99dc16a13b5416d Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 5 Mar 2024 11:27:19 +0000 Subject: [PATCH 264/313] - --- .../main/scala/tests/ChakraComponents.scala | 35 +++++++++----- .../src/main/scala/tests/chakra/Buttons.scala | 19 +++++--- .../main/scala/tests/chakra/ChakraModel.scala | 3 +- .../main/scala/tests/chakra/Editables.scala | 7 ++- .../main/scala/tests/chakra/Navigation.scala | 46 ++++++++----------- .../org/terminal21/client/Controller.scala | 13 ++++-- 6 files changed, 68 insertions(+), 55 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala index 3c458df0..4eebf4c6 100644 --- a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala @@ -12,23 +12,36 @@ import tests.chakra.* Sessions .withNewSession("chakra-components", "Chakra Components") .connect: session => - given ConnectedSession = session - given model: Model[ChakraModel] = Model(ChakraModel()) + given ConnectedSession = session - // react tests reset the session to clear state - val krButton = Button("reset", text = "Reset state").onClick: event => - event.handled.mapModel(_.copy(rerun = true)).terminate + def components(m: ChakraModel, events: Events): MV[ChakraModel] = + // react tests reset the session to clear state + val krButton = Button("reset", text = "Reset state") - def components(m: ChakraModel): Seq[UiElement] = - Overlay.components ++ Forms.components( + val bcs = Buttons.components(m, events) + val elements = Overlay.components(events) ++ Forms.components( + m, + events + ) ++ Editables.components( m - ) ++ Editables.components ++ Stacks.components ++ Grids.components ++ Buttons.components ++ Etc.components ++ MediaAndIcons.components ++ DataDisplay.components ++ Typography.components ++ Feedback.components ++ Disclosure.components ++ - Navigation.components ++ Seq( + ) ++ Stacks.components ++ Grids.components ++ bcs.view ++ Etc.components ++ MediaAndIcons.components ++ DataDisplay.components ++ Typography.components ++ Feedback.components ++ Disclosure.components ++ + Navigation.components(events) ++ Seq( krButton ) - Controller(components(model.value)).render().handledEventsIterator.lastOption.map(_.model) match + + val modifiedModel = bcs.model + val model = modifiedModel.copy( + rerun = events.isClicked(krButton) + ) + MV( + model, + elements, + m.rerun || model.terminate + ) + + Controller(components).render(ChakraModel()).iterator.lastOption.map(_.model) match case Some(m) if m.rerun => - Controller.noModel(Seq(Paragraph(text = "chakra-session-reset"))).render() + Controller.noModel(Seq(Paragraph(text = "chakra-session-reset"))).render(()) Thread.sleep(500) loop() case _ => diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Buttons.scala b/end-to-end-tests/src/main/scala/tests/chakra/Buttons.scala index 8a83044a..5d4c3759 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Buttons.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Buttons.scala @@ -1,18 +1,23 @@ package tests.chakra -import org.terminal21.client.{ConnectedSession, Model} import org.terminal21.client.components.* import org.terminal21.client.components.chakra.* +import org.terminal21.client.* import tests.chakra.Common.* import java.util.concurrent.CountDownLatch object Buttons: - def components(using Model[ChakraModel]): Seq[UiElement] = + def components(m: ChakraModel, events: Events): MV[ChakraModel] = val box1 = commonBox(text = "Buttons") - val exitButton = Button(key = "exit-button", text = "Click to exit program", colorScheme = Some("red")).onClick: event => - event.handled.terminate - Seq( - box1, - exitButton + val exitButton = Button(key = "exit-button", text = "Click to exit program", colorScheme = Some("red")) + val model = m.copy( + terminate = events.isClicked(exitButton) + ) + MV( + model, + Seq( + box1, + exitButton + ) ) diff --git a/end-to-end-tests/src/main/scala/tests/chakra/ChakraModel.scala b/end-to-end-tests/src/main/scala/tests/chakra/ChakraModel.scala index c96b21a4..e9879af6 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/ChakraModel.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/ChakraModel.scala @@ -4,6 +4,5 @@ case class ChakraModel( rerun: Boolean = false, editableStatus: String = "This will reflect any changes in the form.", email: String = "the-test-email@email.com", - breadcrumbStatus: String = "no-breadcrumb-clicked", - linkStatus: String = "no-link-clicked" + terminate: Boolean = false ) diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala b/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala index eecbcd3a..695f0c2e 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala @@ -6,10 +6,7 @@ import org.terminal21.client.components.chakra.* import tests.chakra.Common.* object Editables: - def components(using Model[ChakraModel]): Seq[UiElement] = - val status = Box().onModelChangeRender: (b, m) => - b.withText(m.editableStatus) - + def components(m: ChakraModel): Seq[UiElement] = val editable1 = Editable(key = "editable1", defaultValue = "Please type here") .withChildren( EditablePreview(), @@ -28,6 +25,8 @@ object Editables: import event.* handled.mapModel(_.copy(editableStatus = s"editable2 newValue = $newValue")) + val status = Box(text = m.editableStatus) + Seq( commonBox(text = "Editables"), SimpleGrid(columns = 2).withChildren( diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Navigation.scala b/end-to-end-tests/src/main/scala/tests/chakra/Navigation.scala index a63583b4..0033273a 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Navigation.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Navigation.scala @@ -7,39 +7,33 @@ import org.terminal21.client.components.std.Paragraph import tests.chakra.Common.commonBox object Navigation: - def components(using Model[ChakraModel]): Seq[UiElement] = - val clickedBreadcrumb = Paragraph().onModelChangeRender: (p, m) => - p.withText(m.breadcrumbStatus) - def breadcrumbClicked(m: ChakraModel, t: String) = m.copy(breadcrumbStatus = s"breadcrumb-click: $t") + def components(events: Events): Seq[UiElement] = + val bcLink1 = BreadcrumbLink("breadcrumb-home", text = "breadcrumb-home") + val bcLink2 = BreadcrumbLink("breadcrumb-link1", text = "breadcrumb1") + val bcLink3 = BreadcrumbItem(isCurrentPage = Some(true)) + val bcLink4 = BreadcrumbLink("breadcrumb-link2", text = "breadcrumb2") + val link = Link(key = "google-link", text = "link-external-google", href = "https://www.google.com/", isExternal = Some(true)) - val clickedLink = Paragraph().onModelChangeRender: (p, m) => - p.withText(m.linkStatus) + val bcStatus = + ( + events.ifClicked(bcLink1, "breadcrumb-home").toSeq ++ + events.ifClicked(bcLink2, "breadcrumb-link1") ++ + events.ifClicked(bcLink3, "breadcrumb-link2") ++ + events.ifClicked(bcLink4, "breadcrumb-link2") + ).headOption.getOrElse("no-breadcrumb-clicked") + + val clickedBreadcrumb = Paragraph(text = bcStatus) + val clickedLink = Paragraph(text = if events.isClicked(link) then "link-clicked" else "no-link-clicked") Seq( commonBox(text = "Breadcrumbs"), Breadcrumb().withChildren( - BreadcrumbItem().withChildren( - BreadcrumbLink("breadcrumb-home", text = "breadcrumb-home").onClick: event => - import event.* - handled.withModel(breadcrumbClicked(model, "breadcrumb-home")) - ), - BreadcrumbItem().withChildren( - BreadcrumbLink("breadcrumb-link1", text = "breadcrumb-link1").onClick: event => - import event.* - handled.withModel(breadcrumbClicked(model, "breadcrumb-link1")) - ), - BreadcrumbItem(isCurrentPage = Some(true)).withChildren( - BreadcrumbLink("breadcrumb-link2", text = "breadcrumb-link2").onClick: event => - import event.* - handled.withModel(breadcrumbClicked(model, "breadcrumb-link2")) - ) + BreadcrumbItem().withChildren(bcLink1), + BreadcrumbItem().withChildren(bcLink2), + bcLink3.withChildren(bcLink3) ), clickedBreadcrumb, commonBox(text = "Link"), - Link(key = "google-link", text = "link-external-google", href = "https://www.google.com/", isExternal = Some(true)) - .onClick: event => - import event.* - handled.mapModel(_.copy(linkStatus = "link-clicked")) - , + link, clickedLink ) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 561de543..5b6ce2aa 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -2,7 +2,6 @@ package org.terminal21.client import org.terminal21.client.collections.EventIterator import org.terminal21.client.components.UiElement -import org.terminal21.client.components.chakra.Box import org.terminal21.model.{ClientEvent, CommandEvent, OnChange, OnClick} type ModelViewMaterialized[M] = (M, Events) => MV[M] @@ -14,7 +13,7 @@ class Controller[M]( ): def render(initialModel: M): RenderedController[M] = val mv = materializer(initialModel, Events.Empty) - renderChanges(Seq(mv.view)) + renderChanges(mv.view) new RenderedController(eventIteratorFactory, renderChanges, materializer, mv) object Controller: @@ -22,7 +21,7 @@ object Controller: new Controller(session.eventIterator, session.renderChanges, materializer) def noModel(components: Seq[UiElement])(using session: ConnectedSession) = - apply((Unit, Events) => MV((), Box().withChildren(components*))) + apply((Unit, Events) => MV((), components)) class RenderedController[M]( eventIteratorFactory: => Iterator[CommandEvent], @@ -36,8 +35,9 @@ class RenderedController[M]( .scanLeft(initialMv): (mv, e) => val events = Events(e) val newMv = materializer(mv.model, events) - renderChanges(Seq(newMv.view)) + renderChanges(newMv.view) newMv + .takeWhile(_.isTerminate) ) case class Events(event: CommandEvent): @@ -56,4 +56,7 @@ object Events: val Empty = Events(InitialRender) -case class MV[M](model: M, view: UiElement) +case class MV[M](model: M, view: Seq[UiElement], isTerminate: Boolean = false): + def terminate: MV[M] = copy(isTerminate = true) +object MV: + def apply[M](model: M, view: UiElement): MV[M] = MV(model, Seq(view)) From a9a3bc8297f47ce1bb358426f6e57fcb8cb27a5b Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 5 Mar 2024 11:56:37 +0000 Subject: [PATCH 265/313] - --- .../main/scala/tests/ChakraComponents.scala | 2 +- .../src/main/scala/tests/LoginPage.scala | 66 +++++++--------- .../main/scala/tests/MathJaxComponents.scala | 2 +- .../src/main/scala/tests/NivoComponents.scala | 2 +- .../scala/tests/StateSessionStateBug.scala | 45 ----------- .../src/main/scala/tests/StdComponents.scala | 25 +++--- .../main/scala/tests/chakra/ChakraModel.scala | 1 - .../main/scala/tests/chakra/Editables.scala | 16 ++-- .../src/main/scala/tests/chakra/Forms.scala | 5 +- .../src/test/scala/tests/LoggedInTest.scala | 66 ++++++++-------- .../src/test/scala/tests/LoginPageTest.scala | 78 +++++++++---------- .../org/terminal21/client/Controller.scala | 8 +- 12 files changed, 130 insertions(+), 186 deletions(-) delete mode 100644 end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala diff --git a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala index 4eebf4c6..025139f0 100644 --- a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala @@ -23,7 +23,7 @@ import tests.chakra.* m, events ) ++ Editables.components( - m + events ) ++ Stacks.components ++ Grids.components ++ bcs.view ++ Etc.components ++ MediaAndIcons.components ++ DataDisplay.components ++ Typography.components ++ Feedback.components ++ Disclosure.components ++ Navigation.components(events) ++ Seq( krButton diff --git a/end-to-end-tests/src/main/scala/tests/LoginPage.scala b/end-to-end-tests/src/main/scala/tests/LoginPage.scala index 3ed1500e..c27a06b3 100644 --- a/end-to-end-tests/src/main/scala/tests/LoginPage.scala +++ b/end-to-end-tests/src/main/scala/tests/LoginPage.scala @@ -23,49 +23,37 @@ case class LoginForm(email: String = "my@email.com", pwd: String = "mysecret", s /** The login form. Displays an email and password input and a submit button. When run() it will fill in the Login(email,pwd) model. */ class LoginPage(using session: ConnectedSession): - private given initialModel: Model[LoginForm] = Model(LoginForm()) - val okIcon = CheckCircleIcon(color = Some("green")) - val notOkIcon = WarningTwoIcon(color = Some("red")) - val emailInput = Input(key = "email", `type` = "email", defaultValue = initialModel.value.email) - .onChange: changeEvent => - import changeEvent.* - handled.withModel(model.copy(email = newValue)) + private val initialModel = LoginForm() + val okIcon = CheckCircleIcon(color = Some("green")) + val notOkIcon = WarningTwoIcon(color = Some("red")) + val emailInput = Input(key = "email", `type` = "email", defaultValue = initialModel.email) val submitButton = Button(key = "submit", text = "Submit") - .onClick: clickEvent => - import clickEvent.* - // if the email is invalid, we will not terminate. We also will render an error that will be visible for 2 seconds - val isValidEmail = model.isValidEmail - handled.mapModel(_.copy(submitted = isValidEmail, submittedInvalidEmail = !isValidEmail)) - val passwordInput = Input(key = "password", `type` = "password", defaultValue = initialModel.value.pwd) - .onChange: changeEvent => - import changeEvent.* - handled.withModel(model.copy(pwd = newValue)) + val passwordInput = Input(key = "password", `type` = "password", defaultValue = initialModel.pwd) val errorsBox = Box() val errorMsgInvalidEmail = Paragraph(text = "Invalid Email", style = Map("color" -> "red")) def run(): Option[LoginForm] = controller - .render() - .handledEventsIterator + .render(initialModel) + .iterator .map(_.model) .tapEach: form => println(form) .dropWhile(!_.submitted) .nextOption() - def components: Seq[UiElement] = - Seq( + def components(form: LoginForm, events: Events): MV[LoginForm] = + val view = Seq( QuickFormControl() .withLabel("Email address") .withHelperText("We'll never share your email.") .withInputGroup( InputLeftAddon().withChildren(EmailIcon()), emailInput, - InputRightAddon().onModelChangeRender: (i, m) => - i.withChildren(if m.isValidEmail then okIcon else notOkIcon) + InputRightAddon().withChildren(if form.isValidEmail then okIcon else notOkIcon) ), QuickFormControl() .withLabel("Password") @@ -75,34 +63,35 @@ class LoginPage(using session: ConnectedSession): passwordInput ), submitButton, - errorsBox.onModelChangeRender: (eb, m) => - if m.submittedInvalidEmail then eb.withChildren(errorMsgInvalidEmail) else errorsBox + errorsBox.withChildren(if form.submittedInvalidEmail then errorMsgInvalidEmail else errorsBox) + ) + val isValidEmail = form.isValidEmail + val newForm = form.copy( + email = events.changedValue(emailInput, form.email), + pwd = events.changedValue(passwordInput, form.pwd), + submitted = events.isClicked(submitButton) && isValidEmail, + submittedInvalidEmail = events.isClicked(submitButton) && !isValidEmail + ) + MV( + newForm, + view ) def controller: Controller[LoginForm] = Controller(components) - .onEvent: event => - import event.* - val newModel = model.copy(submittedInvalidEmail = false) - handled.withModel(newModel) class LoggedIn(login: LoginForm)(using session: ConnectedSession): - import Model.Standard.booleanFalseModel val yesButton = Button(key = "yes-button", text = "Yes") - .onClick: e => - e.handled.withModel(true).terminate val noButton = Button(key = "no-button", text = "No") - .onClick: e => - e.handled.withModel(false).terminate val emailDetails = Text(text = s"email : ${login.email}") val passwordDetails = Text(text = s"password : ${login.pwd}") def run(): Option[Boolean] = - controller.render().handledEventsIterator.lastOption.map(_.model) + controller.render(false).iterator.lastOption.map(_.model) - def components: Seq[UiElement] = - Seq( + def components(isYes: Boolean, events: Events): MV[Boolean] = + val view = Seq( Paragraph().withChildren( Text(text = "Are your details correct?"), NewLine(), @@ -112,6 +101,11 @@ class LoggedIn(login: LoginForm)(using session: ConnectedSession): ), HStack().withChildren(yesButton, noButton) ) + MV( + events.isClicked(yesButton), + view, + events.isClicked(yesButton) || events.isClicked(noButton) + ) /** @return * A controller with a boolean value, true if user clicked "Yes", false for "No" diff --git a/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala b/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala index 254bc0e4..4b8ff9c4 100644 --- a/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala @@ -23,5 +23,5 @@ import org.terminal21.client.components.mathjax.* style = Map("backgroundColor" -> "gray") ) ) - Controller.noModel(components).render() + Controller.noModel(components).render(()) session.leaveSessionOpenAfterExiting() diff --git a/end-to-end-tests/src/main/scala/tests/NivoComponents.scala b/end-to-end-tests/src/main/scala/tests/NivoComponents.scala index c1a5b689..d29faf49 100644 --- a/end-to-end-tests/src/main/scala/tests/NivoComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/NivoComponents.scala @@ -12,5 +12,5 @@ import tests.nivo.{ResponsiveBarChart, ResponsiveLineChart} given ConnectedSession = session val components = ResponsiveBarChart() ++ ResponsiveLineChart() - Controller.noModel(components).render() + Controller.noModel(components).render(()) session.leaveSessionOpenAfterExiting() diff --git a/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala b/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala deleted file mode 100644 index 672ada6c..00000000 --- a/end-to-end-tests/src/main/scala/tests/StateSessionStateBug.scala +++ /dev/null @@ -1,45 +0,0 @@ -package tests - -import org.terminal21.client.components.* -import org.terminal21.client.components.chakra.* -import org.terminal21.client.components.std.Paragraph -import org.terminal21.client.* - -import java.util.Date - -@main def stateSessionStateBug(): Unit = - Sessions - .withNewSession("stale-session", "Stale Session") - .connect: session => - given ConnectedSession = session - import Model.Standard.unitModel - - val date = new Date() - val components = Seq( - Paragraph(text = s"Now: $date"), - QuickTable() - .withHeaders("Title", "Value") - .withRows( - Seq( - Seq( - "Date - Editable", - Editable(defaultValue = date.toString) - .withChildren( - EditablePreview(), - EditableInput() - ) - ), - Seq( - "Date - Input", - Input(defaultValue = date.toString) - ), - Seq( - "Date - Std Input", - std.Input(defaultValue = date.toString) - ) - ) - ), - Button(text = "Close").onClick: event => - event.handled.terminate - ) - Controller(components).render().handledEventsIterator.lastOption diff --git a/end-to-end-tests/src/main/scala/tests/StdComponents.scala b/end-to-end-tests/src/main/scala/tests/StdComponents.scala index 7c6040c2..c7b8d145 100644 --- a/end-to-end-tests/src/main/scala/tests/StdComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/StdComponents.scala @@ -10,17 +10,15 @@ import org.terminal21.client.components.std.* .connect: session => given ConnectedSession = session - case class Form(output: String, cookie: String) - given model: Model[Form] = Model(Form("This will reflect what you type in the input", "This will display the value of the cookie")) + def components(events: Events) = + val input = Input(key = "name", defaultValue = "Please enter your name") + val cookieReader = CookieReader(key = "cookie-reader", name = "std-components-test-cookie") - def components = - val output = Paragraph().onModelChangeRender: (p, m) => - p.withText(m.output) - val cookieValue = Paragraph().onModelChangeRender: (p, m) => - p.withText(m.cookie) - val input = Input(key = "name", defaultValue = "Please enter your name").onChange: event => - import event.* - handled.withModel(event.model.copy(output = newValue)) + val outputMsg = events.changedValue(input, "This will reflect what you type in the input") + val output = Paragraph(text = outputMsg) + + val cookieMsg = events.changedValue(cookieReader).map(newValue => s"Cookie value $newValue").getOrElse("This will display the value of the cookie") + val cookieValue = Paragraph(text = cookieMsg) Seq( Header1(text = "header1 test"), @@ -39,11 +37,8 @@ import org.terminal21.client.components.std.* Paragraph(text = "A Form").withChildren(input), output, Cookie(name = "std-components-test-cookie", value = "test-cookie-value"), - CookieReader(key = "cookie-reader", name = "std-components-test-cookie").onChange: event => - import event.* - handled.mapModel(_.copy(cookie = s"Cookie value $newValue")) - , + cookieReader, cookieValue ) - Controller(components).render().handledEventsIterator.lastOption + Controller.noModel(components).render(()).iterator.lastOption diff --git a/end-to-end-tests/src/main/scala/tests/chakra/ChakraModel.scala b/end-to-end-tests/src/main/scala/tests/chakra/ChakraModel.scala index e9879af6..8d5182be 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/ChakraModel.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/ChakraModel.scala @@ -2,7 +2,6 @@ package tests.chakra case class ChakraModel( rerun: Boolean = false, - editableStatus: String = "This will reflect any changes in the form.", email: String = "the-test-email@email.com", terminate: Boolean = false ) diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala b/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala index 695f0c2e..c0e3f5b8 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala @@ -1,31 +1,29 @@ package tests.chakra -import org.terminal21.client.Model import org.terminal21.client.components.UiElement import org.terminal21.client.components.chakra.* +import org.terminal21.client.* import tests.chakra.Common.* object Editables: - def components(m: ChakraModel): Seq[UiElement] = + def components(events: Events): Seq[UiElement] = val editable1 = Editable(key = "editable1", defaultValue = "Please type here") .withChildren( EditablePreview(), EditableInput() ) - .onChange: event => - import event.* - handled.mapModel(_.copy(editableStatus = s"editable1 newValue = $newValue")) val editable2 = Editable(key = "editable2", defaultValue = "For longer maybe-editable texts\nUse an EditableTextarea\nIt uses a textarea control.") .withChildren( EditablePreview(), EditableTextarea() ) - .onChange: event => - import event.* - handled.mapModel(_.copy(editableStatus = s"editable2 newValue = $newValue")) - val status = Box(text = m.editableStatus) + val statusMsg = (events.changedValue(editable1).map(newValue => s"editable1 newValue = $newValue") ++ events + .changedValue(editable2) + .map(newValue => s"editable2 newValue = $newValue")).headOption.getOrElse("This will reflect any changes in the form.") + + val status = Box(text = statusMsg) Seq( commonBox(text = "Editables"), diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala b/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala index 1afc5665..ebb7e3c5 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala @@ -44,7 +44,7 @@ object Forms: val saveButton = Button(key = "save-button", text = "Save", colorScheme = Some("red")) val cancelButton = Button(key = "cancel-button", text = "Cancel") val formStatus = - events + (events .changedValue(email) .map(v => s"email input new value = $v") .toSeq ++ events @@ -65,8 +65,7 @@ object Forms: .changedValue(radioGroup) .map(v => s"radioGroup newValue is $v") ++ events .ifClicked(saveButton, "Saved clicked") ++ events - .ifClicked(cancelButton, "Cancel clicked") - .headOption + .ifClicked(cancelButton, "Cancel clicked")).headOption .getOrElse("This will reflect any changes in the form.") val status = Box(text = formStatus) diff --git a/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala b/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala index c6f2a1a1..0e587c37 100644 --- a/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala +++ b/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala @@ -1,33 +1,33 @@ -package tests - -import org.scalatest.funsuite.AnyFunSuiteLike -import org.scalatest.matchers.should.Matchers.* -import org.terminal21.client.{ConnectedSession, ConnectedSessionMock} -import org.terminal21.model.CommandEvent - -class LoggedInTest extends AnyFunSuiteLike: - class App: - val login = LoginForm() - given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock - val form = new LoggedIn(login) - def allComponents = form.components.flatMap(_.flat) - - test("renders email details"): - new App: - allComponents should contain(form.emailDetails) - - test("renders password details"): - new App: - allComponents should contain(form.passwordDetails) - - test("yes clicked"): - new App: - val eventsIt = form.controller.render().handledEventsIterator - session.fireEvents(CommandEvent.onClick(form.yesButton), CommandEvent.sessionClosed) - eventsIt.lastOption.map(_.model) should be(Some(true)) - - test("no clicked"): - new App: - val eventsIt = form.controller.render().handledEventsIterator - session.fireEvents(CommandEvent.onClick(form.noButton), CommandEvent.sessionClosed) - eventsIt.lastOption.map(_.model) should be(Some(false)) +//package tests +// +//import org.scalatest.funsuite.AnyFunSuiteLike +//import org.scalatest.matchers.should.Matchers.* +//import org.terminal21.client.{ConnectedSession, ConnectedSessionMock} +//import org.terminal21.model.CommandEvent +// +//class LoggedInTest extends AnyFunSuiteLike: +// class App: +// val login = LoginForm() +// given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock +// val form = new LoggedIn(login) +// def allComponents = form.components.flatMap(_.flat) +// +// test("renders email details"): +// new App: +// allComponents should contain(form.emailDetails) +// +// test("renders password details"): +// new App: +// allComponents should contain(form.passwordDetails) +// +// test("yes clicked"): +// new App: +// val eventsIt = form.controller.render().handledEventsIterator +// session.fireEvents(CommandEvent.onClick(form.yesButton), CommandEvent.sessionClosed) +// eventsIt.lastOption.map(_.model) should be(Some(true)) +// +// test("no clicked"): +// new App: +// val eventsIt = form.controller.render().handledEventsIterator +// session.fireEvents(CommandEvent.onClick(form.noButton), CommandEvent.sessionClosed) +// eventsIt.lastOption.map(_.model) should be(Some(false)) diff --git a/end-to-end-tests/src/test/scala/tests/LoginPageTest.scala b/end-to-end-tests/src/test/scala/tests/LoginPageTest.scala index 7b479973..fff813f9 100644 --- a/end-to-end-tests/src/test/scala/tests/LoginPageTest.scala +++ b/end-to-end-tests/src/test/scala/tests/LoginPageTest.scala @@ -1,39 +1,39 @@ -package tests - -import org.scalatest.funsuite.AnyFunSuiteLike -import org.scalatest.matchers.should.Matchers.* -import org.terminal21.client.components.* -import org.terminal21.client.{ConnectedSession, ConnectedSessionMock} -import org.terminal21.model.CommandEvent - -class LoginPageTest extends AnyFunSuiteLike: - - class App: - given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock - val login = LoginForm() - val page = new LoginPage - def allComponents: Seq[UiElement] = page.components.flatMap(_.flat) - - test("renders email input"): - new App: - allComponents should contain(page.emailInput) - - test("renders password input"): - new App: - allComponents should contain(page.passwordInput) - - test("renders submit button"): - new App: - allComponents should contain(page.submitButton) - - test("user submits validated data"): - new App: - val eventsIt = page.controller.render().handledEventsIterator // get the iterator before we fire the events, otherwise the iterator will be empty - session.fireEvents( - CommandEvent.onChange(page.emailInput, "an@email.com"), - CommandEvent.onChange(page.passwordInput, "secret"), - CommandEvent.onClick(page.submitButton), - CommandEvent.sessionClosed // every test should close the session so that the iterator doesn't block if converted to a list. - ) - - eventsIt.lastOption.map(_.model) should be(Some(LoginForm("an@email.com", "secret", true))) +//package tests +// +//import org.scalatest.funsuite.AnyFunSuiteLike +//import org.scalatest.matchers.should.Matchers.* +//import org.terminal21.client.components.* +//import org.terminal21.client.{ConnectedSession, ConnectedSessionMock} +//import org.terminal21.model.CommandEvent +// +//class LoginPageTest extends AnyFunSuiteLike: +// +// class App: +// given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock +// val login = LoginForm() +// val page = new LoginPage +// def allComponents: Seq[UiElement] = page.components.flatMap(_.flat) +// +// test("renders email input"): +// new App: +// allComponents should contain(page.emailInput) +// +// test("renders password input"): +// new App: +// allComponents should contain(page.passwordInput) +// +// test("renders submit button"): +// new App: +// allComponents should contain(page.submitButton) +// +// test("user submits validated data"): +// new App: +// val eventsIt = page.controller.render().handledEventsIterator // get the iterator before we fire the events, otherwise the iterator will be empty +// session.fireEvents( +// CommandEvent.onChange(page.emailInput, "an@email.com"), +// CommandEvent.onChange(page.passwordInput, "secret"), +// CommandEvent.onClick(page.submitButton), +// CommandEvent.sessionClosed // every test should close the session so that the iterator doesn't block if converted to a list. +// ) +// +// eventsIt.lastOption.map(_.model) should be(Some(LoginForm("an@email.com", "secret", true))) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 5b6ce2aa..3689ac19 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -21,7 +21,10 @@ object Controller: new Controller(session.eventIterator, session.renderChanges, materializer) def noModel(components: Seq[UiElement])(using session: ConnectedSession) = - apply((Unit, Events) => MV((), components)) + apply((_, _) => MV((), components)) + + def noModel(materializer: Events => Seq[UiElement])(using session: ConnectedSession) = + apply((_, events) => MV((), materializer(events))) class RenderedController[M]( eventIteratorFactory: => Iterator[CommandEvent], @@ -47,7 +50,8 @@ case class Events(event: CommandEvent): def ifClicked[V](e: UiElement, value: => V): Option[V] = if isClicked(e) then Some(value) else None - def changedValue(e: UiElement): Option[String] = event match + def changedValue(e: UiElement, default: String): String = changedValue(e).getOrElse(default) + def changedValue(e: UiElement): Option[String] = event match case OnChange(key, value) if key == e.key => Some(value) case _ => None From 28cbd51960377a6d8c7f271cd546f905ff2cb1f1 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 5 Mar 2024 12:04:44 +0000 Subject: [PATCH 266/313] - --- .../src/main/scala/tests/LoginPage.scala | 19 ++++++++++--------- .../org/terminal21/client/Controller.scala | 4 +++- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/LoginPage.scala b/end-to-end-tests/src/main/scala/tests/LoginPage.scala index c27a06b3..2b9eaae6 100644 --- a/end-to-end-tests/src/main/scala/tests/LoginPage.scala +++ b/end-to-end-tests/src/main/scala/tests/LoginPage.scala @@ -46,6 +46,14 @@ class LoginPage(using session: ConnectedSession): .nextOption() def components(form: LoginForm, events: Events): MV[LoginForm] = + println(events.event) + val isValidEmail = form.isValidEmail + val newForm = form.copy( + email = events.changedValue(emailInput, form.email), + pwd = events.changedValue(passwordInput, form.pwd), + submitted = events.isClicked(submitButton) && isValidEmail, + submittedInvalidEmail = events.isClicked(submitButton) && !isValidEmail + ) val view = Seq( QuickFormControl() .withLabel("Email address") @@ -53,7 +61,7 @@ class LoginPage(using session: ConnectedSession): .withInputGroup( InputLeftAddon().withChildren(EmailIcon()), emailInput, - InputRightAddon().withChildren(if form.isValidEmail then okIcon else notOkIcon) + InputRightAddon().withChildren(if newForm.isValidEmail then okIcon else notOkIcon) ), QuickFormControl() .withLabel("Password") @@ -63,14 +71,7 @@ class LoginPage(using session: ConnectedSession): passwordInput ), submitButton, - errorsBox.withChildren(if form.submittedInvalidEmail then errorMsgInvalidEmail else errorsBox) - ) - val isValidEmail = form.isValidEmail - val newForm = form.copy( - email = events.changedValue(emailInput, form.email), - pwd = events.changedValue(passwordInput, form.pwd), - submitted = events.isClicked(submitButton) && isValidEmail, - submittedInvalidEmail = events.isClicked(submitButton) && !isValidEmail + errorsBox.withChildren(if newForm.submittedInvalidEmail then errorMsgInvalidEmail else errorsBox) ) MV( newForm, diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 3689ac19..c862fac6 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -40,7 +40,9 @@ class RenderedController[M]( val newMv = materializer(mv.model, events) renderChanges(newMv.view) newMv - .takeWhile(_.isTerminate) + .flatMap: mv => + if mv.isTerminate then Seq(mv.copy(isTerminate = false), mv) else Seq(mv) + .takeWhile(!_.isTerminate) ) case class Events(event: CommandEvent): From 139659d16e37dfe40a40191f7f30e4e6e01b0199 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 5 Mar 2024 12:05:03 +0000 Subject: [PATCH 267/313] - --- .../src/main/scala/org/terminal21/client/Controller.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index c862fac6..e1552855 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -41,6 +41,7 @@ class RenderedController[M]( renderChanges(newMv.view) newMv .flatMap: mv => + // make sure we read the last MV change when terminating if mv.isTerminate then Seq(mv.copy(isTerminate = false), mv) else Seq(mv) .takeWhile(!_.isTerminate) ) From c885931073a6d66dca1e1c2f2cd04a528d9e0f53 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 5 Mar 2024 12:24:39 +0000 Subject: [PATCH 268/313] - --- .../terminal21/client/components/TransientRequest.scala | 6 ------ .../org/terminal21/client/components/std/StdHttp.scala | 8 +++----- 2 files changed, 3 insertions(+), 11 deletions(-) delete mode 100644 terminal21-ui-std/src/main/scala/org/terminal21/client/components/TransientRequest.scala diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/TransientRequest.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/TransientRequest.scala deleted file mode 100644 index 4d45f2a3..00000000 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/TransientRequest.scala +++ /dev/null @@ -1,6 +0,0 @@ -package org.terminal21.client.components - -import java.util.UUID - -object TransientRequest: - def newRequestId(): String = UUID.randomUUID().toString diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala index 2bd7c33a..bc3cb0d8 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala @@ -1,10 +1,8 @@ package org.terminal21.client.components.std import org.terminal21.client.components.OnChangeEventHandler.CanHandleOnChangeEvent -import org.terminal21.client.ConnectedSession -import org.terminal21.client.components.{EventHandler, Keys, OnChangeEventHandler, TransientRequest, UiElement} +import org.terminal21.client.components.{Keys, OnChangeEventHandler, UiElement} import org.terminal21.collections.TypedMap -import org.terminal21.model.OnChange /** Elements mapping to Http functionality */ @@ -29,7 +27,7 @@ case class Cookie( value: String = "cookie.value", path: Option[String] = None, expireDays: Option[Int] = None, - requestId: String = TransientRequest.newRequestId(), + requestId: String = "cookie-set-req", dataStore: TypedMap = TypedMap.empty ) extends StdHttp: override type This = Cookie @@ -43,7 +41,7 @@ case class CookieReader( key: String = Keys.nextKey, name: String = "cookie.name", value: Option[String] = None, // will be set when/if cookie value is read - requestId: String = TransientRequest.newRequestId(), + requestId: String = "cookie-read-req", dataStore: TypedMap = TypedMap.empty ) extends StdHttp with CanHandleOnChangeEvent: From feca8b89d2d941ada159f6c7e1e0d18843e9d299 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 5 Mar 2024 12:30:22 +0000 Subject: [PATCH 269/313] - --- end-to-end-tests/src/main/scala/tests/ChakraComponents.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala index 025139f0..f517146e 100644 --- a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala @@ -36,7 +36,7 @@ import tests.chakra.* MV( model, elements, - m.rerun || model.terminate + model.rerun || model.terminate ) Controller(components).render(ChakraModel()).iterator.lastOption.map(_.model) match From 63384af6d47317a229098b00daf017cd278a14e3 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 5 Mar 2024 12:44:13 +0000 Subject: [PATCH 270/313] - --- .../src/main/scala/tests/chakra/Forms.scala | 2 +- .../main/scala/tests/chakra/Navigation.scala | 21 +++++++++---------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala b/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala index ebb7e3c5..15df5574 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala @@ -63,7 +63,7 @@ object Forms: .changedValue(switch1) .map(v => s"switch1 checked is $v") ++ events .changedValue(radioGroup) - .map(v => s"radioGroup newValue is $v") ++ events + .map(v => s"radioGroup newValue=$v") ++ events .ifClicked(saveButton, "Saved clicked") ++ events .ifClicked(cancelButton, "Cancel clicked")).headOption .getOrElse("This will reflect any changes in the form.") diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Navigation.scala b/end-to-end-tests/src/main/scala/tests/chakra/Navigation.scala index 0033273a..4e155905 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Navigation.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Navigation.scala @@ -8,18 +8,17 @@ import tests.chakra.Common.commonBox object Navigation: def components(events: Events): Seq[UiElement] = - val bcLink1 = BreadcrumbLink("breadcrumb-home", text = "breadcrumb-home") - val bcLink2 = BreadcrumbLink("breadcrumb-link1", text = "breadcrumb1") - val bcLink3 = BreadcrumbItem(isCurrentPage = Some(true)) - val bcLink4 = BreadcrumbLink("breadcrumb-link2", text = "breadcrumb2") - val link = Link(key = "google-link", text = "link-external-google", href = "https://www.google.com/", isExternal = Some(true)) + val bcLinkHome = BreadcrumbLink("breadcrumb-home", text = "breadcrumb-home") + val bcLink1 = BreadcrumbLink("breadcrumb-link1", text = "breadcrumb1") + val bcCurrent = BreadcrumbItem(isCurrentPage = Some(true)) + val bcLink2 = BreadcrumbLink("breadcrumb-link2", text = "breadcrumb2") + val link = Link(key = "google-link", text = "link-external-google", href = "https://www.google.com/", isExternal = Some(true)) val bcStatus = ( - events.ifClicked(bcLink1, "breadcrumb-home").toSeq ++ - events.ifClicked(bcLink2, "breadcrumb-link1") ++ - events.ifClicked(bcLink3, "breadcrumb-link2") ++ - events.ifClicked(bcLink4, "breadcrumb-link2") + events.ifClicked(bcLinkHome, "breadcrumb-click: breadcrumb-home").toSeq ++ + events.ifClicked(bcLink1, "breadcrumb-click: breadcrumb-link1") ++ + events.ifClicked(bcLink2, "breadcrumb-click: breadcrumb-link2") ).headOption.getOrElse("no-breadcrumb-clicked") val clickedBreadcrumb = Paragraph(text = bcStatus) @@ -28,9 +27,9 @@ object Navigation: Seq( commonBox(text = "Breadcrumbs"), Breadcrumb().withChildren( + BreadcrumbItem().withChildren(bcLinkHome), BreadcrumbItem().withChildren(bcLink1), - BreadcrumbItem().withChildren(bcLink2), - bcLink3.withChildren(bcLink3) + bcCurrent.withChildren(bcLink2) ), clickedBreadcrumb, commonBox(text = "Link"), From 61c69788686bdbd31b1b869ca05600383a20e7a1 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 5 Mar 2024 13:08:04 +0000 Subject: [PATCH 271/313] - --- .../terminal21/client/ConnectedSession.scala | 25 ++++++++++++++++++- .../client/json/UiElementEncoding.scala | 2 -- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index 69152678..ca6d8b9c 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -1,5 +1,6 @@ package org.terminal21.client +import io.circe.{Json, JsonNumber, JsonObject} import org.terminal21.client.components.UiElement.HasChildren import org.terminal21.client.components.chakra.Box import org.terminal21.client.components.{UiComponent, UiElement} @@ -107,9 +108,31 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se val j = toJson(es) sessionsService.setSessionJsonState(session, j) // TODO:changeSessionJsonState + private def nullEmptyKeysAndDropNulls(j: Json): Json = + val folder = new Json.Folder[Json] { + def onNull: Json = Json.Null + def onBoolean(value: Boolean): Json = Json.fromBoolean(value) + def onNumber(value: JsonNumber): Json = Json.fromJsonNumber(value) + def onString(value: String): Json = Json.fromString(value) + def onArray(value: Vector[Json]): Json = + Json.fromValues(value.collect { + case v if !v.isNull => v.foldWith(this) + }) + def onObject(value: JsonObject): Json = + Json.fromJsonObject( + value + .filter: + case ("key", v) => !v.asString.contains("") + case (_, v) => !v.isNull + .mapValues(_.foldWith(this)) + ) + } + + j.foldWith(folder) + private def toJson(elementsUn: Seq[UiElement]): ServerJson = val elements = elementsUn.map(_.substituteComponents) val sj = ServerJson( - elements.map(e => encoding.uiElementEncoder(e).deepDropNullValues) + elements.map(e => nullEmptyKeysAndDropNulls(encoding.uiElementEncoder(e))) ) sj diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/json/UiElementEncoding.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/json/UiElementEncoding.scala index fc78fce6..2a6ff9a0 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/json/UiElementEncoding.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/json/UiElementEncoding.scala @@ -41,5 +41,3 @@ object StdElementEncoding extends ComponentLib: case fe: FrontEndElement => fe.asJson.mapObject(o => o.add("type", "FrontEnd".asJson)) case _: UiComponent => throw new IllegalStateException("substitute all components before serializing") -// val b: ChakraElement = Box(key = c.key, text = "") -// b.asJson.mapObject(o => o.add("type", "Chakra".asJson)) From d9547c57d91a0535ff7bf2c8c5988c4a22a35ca0 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 5 Mar 2024 13:29:25 +0000 Subject: [PATCH 272/313] - --- .../serverapp/bundled/AppManager.scala | 9 +++----- .../client/components/chakra/QuickTable.scala | 21 +++++++++++-------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala index 45bb415f..75ab80bf 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala @@ -31,7 +31,7 @@ class AppManagerPage(apps: Seq[ServerSideApp], startApp: ServerSideApp => Unit)( private case class TableView(clicked: Option[ServerSideApp], columns: Seq[UiElement]) private def appRows(events: Events): Seq[TableView] = apps.map: app => - val link = Link(key = s"app-${app.name}", text = app.name) + val link = Link(s"app-${app.name}", text = app.name) TableView( if events.isClicked(link) then Some(app) else None, Seq( @@ -50,12 +50,9 @@ class AppManagerPage(apps: Seq[ServerSideApp], startApp: ServerSideApp => Unit)( val startApp = appsMv.map(_.clicked).find(_.nonEmpty).flatten MV( model.copy(startApp = startApp), - Box().withChildren( + Seq( Header1(text = "Terminal 21 Manager"), - Paragraph( - text = """ - |Here you can run all the installed apps on the server.""".stripMargin - ), + Paragraph(text = "Here you can run all the installed apps on the server."), appsTable, Paragraph().withChildren( Span(text = "Have a question? Please ask at "), diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala index 7bba698a..2f73c735 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala @@ -44,16 +44,19 @@ case class QuickTable( ) val body = Tbody( key = subKey("tb"), - children = rows.map: row => - Tr(children = row.zipWithIndex.map: (c, i) => - Td( - key = subKey(s"tb-th-$i"), - children = Seq( - c match - case u: UiElement => u - case c => Text(text = c.toString) + children = rows.zipWithIndex.map: (row, i) => + Tr( + key = subKey(s"tb-tr-$i"), + children = row.zipWithIndex.map: (c, i) => + Td( + key = subKey(s"tb-th-$i"), + children = Seq( + c match + case u: UiElement => u + case c => Text(text = c.toString) + ) ) - )) + ) ) val table = Table( key = subKey("t"), From 2e0a5d3d58025db725b971ba51e9fd7563313a47 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 5 Mar 2024 13:32:10 +0000 Subject: [PATCH 273/313] - --- .../src/test/scala/tests/LoggedInTest.scala | 66 ++++++++-------- .../src/test/scala/tests/LoginPageTest.scala | 78 +++++++++---------- 2 files changed, 72 insertions(+), 72 deletions(-) diff --git a/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala b/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala index 0e587c37..a8d21a86 100644 --- a/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala +++ b/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala @@ -1,33 +1,33 @@ -//package tests -// -//import org.scalatest.funsuite.AnyFunSuiteLike -//import org.scalatest.matchers.should.Matchers.* -//import org.terminal21.client.{ConnectedSession, ConnectedSessionMock} -//import org.terminal21.model.CommandEvent -// -//class LoggedInTest extends AnyFunSuiteLike: -// class App: -// val login = LoginForm() -// given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock -// val form = new LoggedIn(login) -// def allComponents = form.components.flatMap(_.flat) -// -// test("renders email details"): -// new App: -// allComponents should contain(form.emailDetails) -// -// test("renders password details"): -// new App: -// allComponents should contain(form.passwordDetails) -// -// test("yes clicked"): -// new App: -// val eventsIt = form.controller.render().handledEventsIterator -// session.fireEvents(CommandEvent.onClick(form.yesButton), CommandEvent.sessionClosed) -// eventsIt.lastOption.map(_.model) should be(Some(true)) -// -// test("no clicked"): -// new App: -// val eventsIt = form.controller.render().handledEventsIterator -// session.fireEvents(CommandEvent.onClick(form.noButton), CommandEvent.sessionClosed) -// eventsIt.lastOption.map(_.model) should be(Some(false)) +package tests + +import org.scalatest.funsuite.AnyFunSuiteLike +import org.scalatest.matchers.should.Matchers.* +import org.terminal21.client.* +import org.terminal21.model.CommandEvent + +class LoggedInTest extends AnyFunSuiteLike: + class App: + val login = LoginForm() + given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock + val form = new LoggedIn(login) + def allComponents = form.components(false, Events.Empty).view.flatMap(_.flat) + + test("renders email details"): + new App: + allComponents should contain(form.emailDetails) + + test("renders password details"): + new App: + allComponents should contain(form.passwordDetails) + + test("yes clicked"): + new App: + val eventsIt = form.controller.render(false).iterator + session.fireEvents(CommandEvent.onClick(form.yesButton), CommandEvent.sessionClosed) + eventsIt.lastOption.map(_.model) should be(Some(true)) + + test("no clicked"): + new App: + val eventsIt = form.controller.render(false).iterator + session.fireEvents(CommandEvent.onClick(form.noButton), CommandEvent.sessionClosed) + eventsIt.lastOption.map(_.model) should be(Some(false)) diff --git a/end-to-end-tests/src/test/scala/tests/LoginPageTest.scala b/end-to-end-tests/src/test/scala/tests/LoginPageTest.scala index fff813f9..ff86244e 100644 --- a/end-to-end-tests/src/test/scala/tests/LoginPageTest.scala +++ b/end-to-end-tests/src/test/scala/tests/LoginPageTest.scala @@ -1,39 +1,39 @@ -//package tests -// -//import org.scalatest.funsuite.AnyFunSuiteLike -//import org.scalatest.matchers.should.Matchers.* -//import org.terminal21.client.components.* -//import org.terminal21.client.{ConnectedSession, ConnectedSessionMock} -//import org.terminal21.model.CommandEvent -// -//class LoginPageTest extends AnyFunSuiteLike: -// -// class App: -// given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock -// val login = LoginForm() -// val page = new LoginPage -// def allComponents: Seq[UiElement] = page.components.flatMap(_.flat) -// -// test("renders email input"): -// new App: -// allComponents should contain(page.emailInput) -// -// test("renders password input"): -// new App: -// allComponents should contain(page.passwordInput) -// -// test("renders submit button"): -// new App: -// allComponents should contain(page.submitButton) -// -// test("user submits validated data"): -// new App: -// val eventsIt = page.controller.render().handledEventsIterator // get the iterator before we fire the events, otherwise the iterator will be empty -// session.fireEvents( -// CommandEvent.onChange(page.emailInput, "an@email.com"), -// CommandEvent.onChange(page.passwordInput, "secret"), -// CommandEvent.onClick(page.submitButton), -// CommandEvent.sessionClosed // every test should close the session so that the iterator doesn't block if converted to a list. -// ) -// -// eventsIt.lastOption.map(_.model) should be(Some(LoginForm("an@email.com", "secret", true))) +package tests + +import org.scalatest.funsuite.AnyFunSuiteLike +import org.scalatest.matchers.should.Matchers.* +import org.terminal21.client.components.* +import org.terminal21.client.* +import org.terminal21.model.CommandEvent + +class LoginPageTest extends AnyFunSuiteLike: + + class App: + given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock + val login = LoginForm() + val page = new LoginPage + def allComponents: Seq[UiElement] = page.components(login, Events.Empty).view.flatMap(_.flat) + + test("renders email input"): + new App: + allComponents should contain(page.emailInput) + + test("renders password input"): + new App: + allComponents should contain(page.passwordInput) + + test("renders submit button"): + new App: + allComponents should contain(page.submitButton) + + test("user submits validated data"): + new App: + val eventsIt = page.controller.render(login).iterator // get the iterator before we fire the events, otherwise the iterator will be empty + session.fireEvents( + CommandEvent.onChange(page.emailInput, "an@email.com"), + CommandEvent.onChange(page.passwordInput, "secret"), + CommandEvent.onClick(page.submitButton), + CommandEvent.sessionClosed // every test should close the session so that the iterator doesn't block if converted to a list. + ) + + eventsIt.lastOption.map(_.model) should be(Some(LoginForm("an@email.com", "secret", true))) From 3e2b354bc2f81b693ce54b3db900e6d8cfdc3fba Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 5 Mar 2024 13:36:30 +0000 Subject: [PATCH 274/313] - --- .../sparklib/CalculationsExtensions.scala | 3 +-- .../calculations/SparkCalculation.scala | 4 ++-- .../client/components/StdUiCalculation.scala | 17 ++++++++--------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/CalculationsExtensions.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/CalculationsExtensions.scala index 3e12323c..a5408c0a 100644 --- a/terminal21-spark/src/main/scala/org/terminal21/sparklib/CalculationsExtensions.scala +++ b/terminal21-spark/src/main/scala/org/terminal21/sparklib/CalculationsExtensions.scala @@ -2,7 +2,7 @@ package org.terminal21.sparklib import functions.fibers.FiberExecutor import org.apache.spark.sql.SparkSession -import org.terminal21.client.{ConnectedSession, Model} +import org.terminal21.client.* import org.terminal21.client.components.UiElement.HasStyle import org.terminal21.client.components.{Keys, UiElement} import org.terminal21.sparklib.calculations.{ReadWriter, StdUiSparkCalculation} @@ -12,7 +12,6 @@ extension [OUT: ReadWriter](ds: OUT) toUi: OUT => UiElement & HasStyle )(using ConnectedSession, - Model[_], FiberExecutor, SparkSession ) = diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala index e3103d32..19eb2bea 100644 --- a/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala +++ b/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala @@ -3,7 +3,7 @@ package org.terminal21.sparklib.calculations import functions.fibers.FiberExecutor import org.apache.commons.io.FileUtils import org.apache.spark.sql.SparkSession -import org.terminal21.client.{ConnectedSession, Model} +import org.terminal21.client.* import org.terminal21.client.components.UiElement.HasStyle import org.terminal21.client.components.{CachedCalculation, StdUiCalculation, UiComponent, UiElement} import org.terminal21.sparklib.util.Environment @@ -49,6 +49,6 @@ abstract class StdUiSparkCalculation[OUT: ReadWriter]( override val key: String, name: String, dataUi: UiElement with HasStyle -)(using ConnectedSession, Model[_], FiberExecutor, SparkSession) +)(using ConnectedSession, FiberExecutor, SparkSession) extends StdUiCalculation[OUT](key, name, dataUi) with SparkCalculation[OUT](name) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala index d36904a6..2b1ff1a8 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala @@ -1,7 +1,7 @@ //package org.terminal21.client.components // //import functions.fibers.FiberExecutor -//import org.terminal21.client.{ConnectedSession, Model, RenderChangesEvent} +//import org.terminal21.client.* //import org.terminal21.client.components.UiElement.HasStyle //import org.terminal21.client.components.chakra.* //import org.terminal21.collections.TypedMap @@ -21,20 +21,19 @@ //)(using session: ConnectedSession, executor: FiberExecutor) // extends Calculation[OUT] // with UiComponent: -// private def model = Model.Standard.unitModel // private val running = new AtomicBoolean(false) // private val currentUi = new AtomicReference(dataUi) // // protected def updateUi(dataUi: UiElement & HasStyle) = currentUi.set(dataUi) // // lazy val badge = Badge() -// lazy val recalc = Button(text = "Recalculate", size = Some("sm"), leftIcon = Some(RepeatIcon())).onClick(model): event => -// import event.* -// if running.compareAndSet(false, true) then -// try -// reCalculate() -// finally running.set(false) -// handled +// lazy val recalc = Button(text = "Recalculate", size = Some("sm"), leftIcon = Some(RepeatIcon())) +// // .onClick(model) +// // if running.compareAndSet(false, true) then +// // try +// // reCalculate() +// // finally running.set(false) +// // handled // // override lazy val rendered: Seq[UiElement] = // val header = Box( From 077e857be8e987bbf59a7cc9a407e524c14fee0f Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 5 Mar 2024 15:07:19 +0000 Subject: [PATCH 275/313] - --- .../main/scala/tests/MathJaxComponents.scala | 2 +- .../src/main/scala/tests/NivoComponents.scala | 2 +- .../src/main/scala/tests/StdComponents.scala | 2 +- example-scripts/bouncing-ball.sc | 19 +++++----- example-scripts/csv-viewer.sc | 35 ++++++++++--------- example-scripts/hello-world.sc | 6 +--- .../sparklib/endtoend/SparkBasics.scala | 1 - .../org/terminal21/client/Controller.scala | 11 ++++-- 8 files changed, 40 insertions(+), 38 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala b/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala index 4b8ff9c4..254bc0e4 100644 --- a/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala @@ -23,5 +23,5 @@ import org.terminal21.client.components.mathjax.* style = Map("backgroundColor" -> "gray") ) ) - Controller.noModel(components).render(()) + Controller.noModel(components).render() session.leaveSessionOpenAfterExiting() diff --git a/end-to-end-tests/src/main/scala/tests/NivoComponents.scala b/end-to-end-tests/src/main/scala/tests/NivoComponents.scala index d29faf49..c1a5b689 100644 --- a/end-to-end-tests/src/main/scala/tests/NivoComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/NivoComponents.scala @@ -12,5 +12,5 @@ import tests.nivo.{ResponsiveBarChart, ResponsiveLineChart} given ConnectedSession = session val components = ResponsiveBarChart() ++ ResponsiveLineChart() - Controller.noModel(components).render(()) + Controller.noModel(components).render() session.leaveSessionOpenAfterExiting() diff --git a/end-to-end-tests/src/main/scala/tests/StdComponents.scala b/end-to-end-tests/src/main/scala/tests/StdComponents.scala index c7b8d145..4f8b14f2 100644 --- a/end-to-end-tests/src/main/scala/tests/StdComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/StdComponents.scala @@ -41,4 +41,4 @@ import org.terminal21.client.components.std.* cookieValue ) - Controller.noModel(components).render(()).iterator.lastOption + Controller.noModel(components).render().iterator.lastOption diff --git a/example-scripts/bouncing-ball.sc b/example-scripts/bouncing-ball.sc index acd9d229..372f6f43 100755 --- a/example-scripts/bouncing-ball.sc +++ b/example-scripts/bouncing-ball.sc @@ -25,23 +25,24 @@ Sessions Ball(x + newDx, y + newDy, newDx, newDy) case object Ticker extends ClientEvent - given Model[Ball] = Model(Ball(50, 50, 8, 8)) + val initialModel = Ball(50, 50, 8, 8) println( "Files under ~/.terminal21/web will be served under /web . Please place a ball.png file under ~/.terminal21/web/images on the box where the server runs." ) - val ball = Image(src = "/web/images/ball.png").onModelChange: (b, m) => - b.withStyle("position" -> "fixed", "left" -> (m.x + "px"), "top" -> (m.y + "px")) + def components(ball: Ball, events: Events): MV[Ball] = + val b = ball.nextPosition + MV( + b, + Image(src = "/web/images/ball.png").withStyle("position" -> "fixed", "left" -> (b.x + "px"), "top" -> (b.y + "px")) + ) fiberExecutor.submit: while !session.isClosed do session.fireEvent(Ticker) Thread.sleep(1000 / 60) - Controller(Seq(ball)) - .onEvent: - case ControllerClientEvent(handled, Ticker) => - handled.withModel(handled.model.nextPosition) - .render() - .handledEventsIterator + Controller(components) + .render(initialModel) + .iterator .lastOption diff --git a/example-scripts/csv-viewer.sc b/example-scripts/csv-viewer.sc index 5a38521a..b14c61cf 100755 --- a/example-scripts/csv-viewer.sc +++ b/example-scripts/csv-viewer.sc @@ -31,24 +31,25 @@ Sessions .withNewSession(s"csv-viewer-$fileName", s"CsvView: $fileName") .connect: session => given ConnectedSession = session - given Model[Unit] = Model.Standard.unitModel - - Controller( - TableContainer() - .withChildren( - Table(variant = "striped", colorScheme = Some("teal"), size = "mg") - .withChildren( - TableCaption(text = "Csv file contents"), - Tbody( - children = csv.map: row => - Tr( - children = row.map: column => - Td(text = column) - ) + + Controller + .noModel( + TableContainer() + .withChildren( + Table(variant = "striped", colorScheme = Some("teal"), size = "mg") + .withChildren( + TableCaption(text = "Csv file contents"), + Tbody( + children = csv.map: row => + Tr( + children = row.map: column => + Td(text = column) + ) + ) ) - ) - ) - ).render() // we don't have to process any events here, just let the user view the csv file. + ) + ) + .render() // we don't have to process any events here, just let the user view the csv file. println(s"Now open ${session.uiUrl} to view the UI.") // since this is a read-only UI, we can exit the app but leave the session open on the UI for the user to examine the data. session.leaveSessionOpenAfterExiting() diff --git a/example-scripts/hello-world.sc b/example-scripts/hello-world.sc index eecaf3b1..aea2f55b 100755 --- a/example-scripts/hello-world.sc +++ b/example-scripts/hello-world.sc @@ -9,14 +9,10 @@ import org.terminal21.client.components.* // std components, https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdElement.scala import org.terminal21.client.components.std.* -// We don't have a model in this simple example, so we will import the standard Unit model -// for our controller to use. -import org.terminal21.client.Model.Standard.unitModel - Sessions .withNewSession("hello-world", "Hello World Example") .connect: session => given ConnectedSession = session - Controller(Seq(Paragraph(text = "Hello World!"))).render() + Controller.noModel(Seq(Paragraph(text = "Hello World!"))).render(()) session.leaveSessionOpenAfterExiting() diff --git a/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala b/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala index 600fc8cf..61e55152 100644 --- a/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala +++ b/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala @@ -20,7 +20,6 @@ import scala.util.Using .connect: session => given ConnectedSession = session given SparkSession = spark - given Model[Unit] = Model.Standard.unitModel import scala3encoders.given import spark.implicits.* diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index e1552855..39a64ef1 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -16,15 +16,20 @@ class Controller[M]( renderChanges(mv.view) new RenderedController(eventIteratorFactory, renderChanges, materializer, mv) +trait NoModelController: + this: Controller[Unit] => + def render(): RenderedController[Unit] = render(()) + object Controller: def apply[M](materializer: ModelViewMaterialized[M])(using session: ConnectedSession): Controller[M] = new Controller(session.eventIterator, session.renderChanges, materializer) - def noModel(components: Seq[UiElement])(using session: ConnectedSession) = - apply((_, _) => MV((), components)) + def noModel(component: UiElement)(using session: ConnectedSession): Controller[Unit] with NoModelController = noModel(Seq(component)) + def noModel(components: Seq[UiElement])(using session: ConnectedSession): Controller[Unit] with NoModelController = + new Controller[Unit](session.eventIterator, session.renderChanges, (_, _) => MV((), components)) with NoModelController def noModel(materializer: Events => Seq[UiElement])(using session: ConnectedSession) = - apply((_, events) => MV((), materializer(events))) + new Controller[Unit](session.eventIterator, session.renderChanges, (_, events) => MV((), materializer(events))) with NoModelController class RenderedController[M]( eventIteratorFactory: => Iterator[CommandEvent], From e7d2957befa5b14f0c24e39cb42be05f29f184b0 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 5 Mar 2024 15:12:04 +0000 Subject: [PATCH 276/313] - --- example-scripts/hello-world.sc | 2 +- example-scripts/mathjax.sc | 22 ++++++++++------------ 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/example-scripts/hello-world.sc b/example-scripts/hello-world.sc index aea2f55b..e2257180 100755 --- a/example-scripts/hello-world.sc +++ b/example-scripts/hello-world.sc @@ -14,5 +14,5 @@ Sessions .connect: session => given ConnectedSession = session - Controller.noModel(Seq(Paragraph(text = "Hello World!"))).render(()) + Controller.noModel(Paragraph(text = "Hello World!")).render() session.leaveSessionOpenAfterExiting() diff --git a/example-scripts/mathjax.sc b/example-scripts/mathjax.sc index 8e65f2e5..9617bc88 100755 --- a/example-scripts/mathjax.sc +++ b/example-scripts/mathjax.sc @@ -4,22 +4,19 @@ import org.terminal21.client.* import org.terminal21.client.components.* import org.terminal21.client.components.mathjax.* -// We don't have a model in this simple example, so we will import the standard Unit model -// for our controller to use. -import org.terminal21.client.Model.Standard.unitModel - Sessions .withNewSession("mathjax", "MathJax Example") .andLibraries(MathJaxLib /* note we need to register the MathJaxLib in order to use it */ ) .connect: session => given ConnectedSession = session - Controller( - Seq( - MathJax( - expression = """When \(a \ne 0\), there are two solutions to \(ax^2 + bx + c = 0\) and they are $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$""" - ), - MathJax( - expression = """ + Controller + .noModel( + Seq( + MathJax( + expression = """When \(a \ne 0\), there are two solutions to \(ax^2 + bx + c = 0\) and they are $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$""" + ), + MathJax( + expression = """ |when \(a \ne 0\), there are two solutions to \(x = {-b \pm \sqrt{b^2-4ac} \over 2a}.\) |Aenean vel velit a lacus lacinia pulvinar. Morbi eget ex et tellus aliquam molestie sit amet eu diam. |Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas tellus enim, tempor non efficitur et, rutrum efficitur metus. @@ -29,7 +26,8 @@ Sessions |Morbi ultrices sem quis nisl convallis, ac cursus nunc condimentum. Orci varius natoque penatibus et magnis dis parturient montes, |nascetur ridiculus mus. |""".stripMargin + ) ) ) - ).render() + .render() session.leaveSessionOpenAfterExiting() From 805450cdb11907b6cb8ee8b83a82b6bdec6eba4c Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 5 Mar 2024 16:10:01 +0000 Subject: [PATCH 277/313] - --- example-scripts/csv-editor.sc | 100 +++++++++++------- .../org/terminal21/client/Controller.scala | 4 + 2 files changed, 67 insertions(+), 37 deletions(-) diff --git a/example-scripts/csv-editor.sc b/example-scripts/csv-editor.sc index c41fb93b..f13c2750 100755 --- a/example-scripts/csv-editor.sc +++ b/example-scripts/csv-editor.sc @@ -7,6 +7,7 @@ // always import these import org.terminal21.client.* import org.terminal21.client.components.* +import org.terminal21.collections.TypedMapKey import org.terminal21.model.* // use the chakra components for menus, forms etc, https://chakra-ui.com/docs/components // The scala case classes : https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala @@ -26,7 +27,7 @@ val contents = if file.exists() then FileUtils.readFileToString(file, "UTF-8") else "type,damage points,hit points\nmage,10dp,20hp\nwarrior,20dp,30hp" -val csv: Seq[Seq[String]] = contents.split("\n").map(_.split(",").toSeq).toSeq +val csv = toCsvModel(contents.split("\n").map(_.split(",").toSeq).toSeq) Sessions .withNewSession(s"csv-editor-$fileName", s"CsvEdit: $fileName") @@ -36,44 +37,66 @@ Sessions val editor = new CsvEditor(csv) editor.run() -class CsvEditor(csv: Seq[Seq[String]])(using session: ConnectedSession): - /** Our model. If the user clicks "Save", we'll set `save` to true and store the csv data into `csv` - */ - case class CsvModel(save: Boolean, exitWithoutSave: Boolean, csv: Seq[Seq[String]]) - - private given Model[CsvModel] = Model(CsvModel(false, false, Nil)) - - val saveAndExit = Button(text = "Save & Exit").onClick: event => - import event.* - val csv = tableCells.map: row => - row.map: editable => - editable.current.value - handled.withModel(CsvModel(true, false, csv)).terminate // terminate the event iteration after storing the data into the model - val exit = Button(text = "Exit Without Saving").onClick: event => - import event.* - handled.withModel(model.copy(exitWithoutSave = true)).terminate - val status = Box() - - val tableCells = - csv.map: row => - row.map: column => - newEditable(column) +/** Our model. If the user clicks "Save", we'll set `save` to true and store the csv data into `csv` + */ +case class CsvModel(save: Boolean, exitWithoutSave: Boolean, csv: Map[(Int, Int), String], maxX: Int, maxY: Int, status: String = "Please edit the file.") +def toCsvModel(csv: Seq[Seq[String]]) = + val maxX = csv.map(_.size).max + val maxY = csv.size + val m = csv.zipWithIndex + .flatMap: (row, y) => + row.zipWithIndex.map: (column, x) => + ((x, y), column) + .toMap + CsvModel(false, false, m, maxX, maxY) + +class CsvEditor(initModel: CsvModel)(using session: ConnectedSession): + + val saveAndExit = Button(key = "save-exit", text = "Save & Exit") + val exit = Button(key = "exit", text = "Exit Without Saving") def run(): Unit = - for handled <- controller.render().handledEventsIterator.lastOption.filter(_.model.save) // only save if model.save is true - do save(handled.model.csv) - - def components: Seq[UiElement] = - Seq( - QuickTable(variant = "striped", colorScheme = "teal", size = "mg") - .withCaption("Please edit the csv contents above and click save to save and exit") - .withRows(tableCells), + for mv <- controller.render(initModel).iterator.lastOption.filter(_.model.save) // only save if model.save is true + do save(mv.model) + + def cellsComponent(model: CsvModel, events: Events): MV[CsvModel] = + val tableCells = + (0 until model.maxY).map: y => + (0 until model.maxX).map: x => + newEditable(x, y, model.csv(x, y)) + + val view = QuickTable(variant = "striped", colorScheme = "teal", size = "mg") + .withCaption("Please edit the csv contents above and click save to save and exit") + .withRows(tableCells) + + tableCells.flatten.find(events.isChangedValue) match + case Some(editable) => + val coords = editable.dataStore(CoordsKey) + val newValue = events.changedValue(editable, "error") + MV( + model.copy(csv = model.csv + (coords -> newValue), status = s"Changed value at $coords to $newValue"), + view + ) + case None => MV(model, view) + + def components(model: CsvModel, events: Events): MV[CsvModel] = + val cells = cellsComponent(model, events) + val newModel = cells.model.copy( + save = events.isClicked(saveAndExit), + exitWithoutSave = events.isClicked(exit) + ) + val view = cells.view ++ Seq( HStack().withChildren( saveAndExit, exit, - status + Box(text = newModel.status) ) ) + MV( + newModel, + view, + isTerminate = events.isClicked(saveAndExit) || events.isClicked(exit) + ) /** @return * true if the user clicked "Save", false if the user clicked "Exit" or closed the session @@ -81,15 +104,18 @@ class CsvEditor(csv: Seq[Seq[String]])(using session: ConnectedSession): def controller: Controller[CsvModel] = Controller(components) - def save(data: Seq[Seq[String]]): Unit = + def save(model: CsvModel): Unit = + val data = (0 until model.maxY).map: y => + (0 until model.maxX).map: x => + model.csv((x, y)) FileUtils.writeStringToFile(file, data.map(_.mkString(",")).mkString("\n"), "UTF-8") println(s"Csv file saved to $file") - private def newEditable(value: String): Editable = - Editable(defaultValue = value) + object CoordsKey extends TypedMapKey[(Int, Int)] + private def newEditable(x: Int, y: Int, value: String): Editable = + Editable(key = s"cell-$x-$y", defaultValue = value) .withChildren( EditablePreview(), EditableInput() ) - .onChange: event => - event.handled.withRenderChanges(status.withText(s"Changed a cell value to ${event.newValue}")) + .store(CoordsKey, (x, y)) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 39a64ef1..aec8b324 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -62,6 +62,10 @@ case class Events(event: CommandEvent): def changedValue(e: UiElement): Option[String] = event match case OnChange(key, value) if key == e.key => Some(value) case _ => None + def isChangedValue(e: UiElement): Boolean = + event match + case OnChange(key, _) => key == e.key + case _ => false object Events: case object InitialRender extends ClientEvent From edfb9781c35438140afa7086dbbf1f3bfcddf561 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 5 Mar 2024 16:21:21 +0000 Subject: [PATCH 278/313] - --- example-scripts/csv-editor.sc | 25 ++++++++----------- .../client/components/UiElement.scala | 1 + 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/example-scripts/csv-editor.sc b/example-scripts/csv-editor.sc index f13c2750..4d32b000 100755 --- a/example-scripts/csv-editor.sc +++ b/example-scripts/csv-editor.sc @@ -65,19 +65,18 @@ class CsvEditor(initModel: CsvModel)(using session: ConnectedSession): (0 until model.maxX).map: x => newEditable(x, y, model.csv(x, y)) - val view = QuickTable(variant = "striped", colorScheme = "teal", size = "mg") + val newModel = tableCells.flatten.find(events.isChangedValue) match + case Some(editable) => + val coords = editable.storedValue(CoordsKey) + val newValue = events.changedValue(editable, "error") + model.copy(csv = model.csv + (coords -> newValue), status = s"Changed value at $coords to $newValue") + case None => model + + val view = QuickTable(key = "csv-editor", variant = "striped", colorScheme = "teal", size = "mg") .withCaption("Please edit the csv contents above and click save to save and exit") .withRows(tableCells) - tableCells.flatten.find(events.isChangedValue) match - case Some(editable) => - val coords = editable.dataStore(CoordsKey) - val newValue = events.changedValue(editable, "error") - MV( - model.copy(csv = model.csv + (coords -> newValue), status = s"Changed value at $coords to $newValue"), - view - ) - case None => MV(model, view) + MV(newModel, view) def components(model: CsvModel, events: Events): MV[CsvModel] = val cells = cellsComponent(model, events) @@ -98,11 +97,7 @@ class CsvEditor(initModel: CsvModel)(using session: ConnectedSession): isTerminate = events.isClicked(saveAndExit) || events.isClicked(exit) ) - /** @return - * true if the user clicked "Save", false if the user clicked "Exit" or closed the session - */ - def controller: Controller[CsvModel] = - Controller(components) + def controller: Controller[CsvModel] = Controller(components) def save(model: CsvModel): Unit = val data = (0 until model.maxY).map: y => diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala index 63f314ad..468022a1 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala @@ -14,6 +14,7 @@ abstract class UiElement extends AnyElement: def dataStore: TypedMap def withDataStore(ds: TypedMap): This def store[V](key: TypedMapKey[V], value: V): This = withDataStore(dataStore + (key -> value)) + def storedValue[V](key: TypedMapKey[V]): V = dataStore(key) /** @return * this element along all it's children flattened From 7804ba102fd25fa58b97ce711cebf0046aa91154 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 5 Mar 2024 16:31:41 +0000 Subject: [PATCH 279/313] - --- example-scripts/csv-editor.sc | 2 +- .../org/terminal21/client/Controller.scala | 8 +++---- .../terminal21/client/ControllerTest.scala | 22 +++++++++---------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/example-scripts/csv-editor.sc b/example-scripts/csv-editor.sc index 4d32b000..154c07d9 100755 --- a/example-scripts/csv-editor.sc +++ b/example-scripts/csv-editor.sc @@ -94,7 +94,7 @@ class CsvEditor(initModel: CsvModel)(using session: ConnectedSession): MV( newModel, view, - isTerminate = events.isClicked(saveAndExit) || events.isClicked(exit) + isTerminate = newModel.exitWithoutSave || newModel.save ) def controller: Controller[CsvModel] = Controller(components) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index aec8b324..767bdd2a 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -47,8 +47,8 @@ class RenderedController[M]( newMv .flatMap: mv => // make sure we read the last MV change when terminating - if mv.isTerminate then Seq(mv.copy(isTerminate = false), mv) else Seq(mv) - .takeWhile(!_.isTerminate) + if mv.terminate then Seq(mv.copy(terminate = false), mv) else Seq(mv) + .takeWhile(!_.terminate) ) case class Events(event: CommandEvent): @@ -72,7 +72,7 @@ object Events: val Empty = Events(InitialRender) -case class MV[M](model: M, view: Seq[UiElement], isTerminate: Boolean = false): - def terminate: MV[M] = copy(isTerminate = true) +case class MV[M](model: M, view: Seq[UiElement], terminate: Boolean = false) + object MV: def apply[M](model: M, view: UiElement): MV[M] = MV(model, Seq(view)) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 32ee9499..6a0c4fa3 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -1,17 +1,14 @@ package org.terminal21.client import org.mockito.Mockito -import org.mockito.Mockito.verify import org.scalatest.funsuite.AnyFunSuiteLike -import org.scalatest.matchers.should.Matchers.* import org.scalatestplus.mockito.MockitoSugar.* import org.terminal21.client.components.UiElement -import org.terminal21.client.components.chakra.{Box, Button, Checkbox, QuickTable, Text} -import org.terminal21.client.components.std.{Input, Paragraph} +import org.terminal21.client.components.chakra.* +import org.terminal21.client.components.std.Input import org.terminal21.collections.SEList -import org.terminal21.model.{ClientEvent, CommandEvent, OnChange, OnClick} - -import java.util.concurrent.atomic.AtomicBoolean +import org.terminal21.model.{CommandEvent, OnChange, OnClick} +import org.scalatest.matchers.should.Matchers.* class ControllerTest extends AnyFunSuiteLike: val button = Button("b1") @@ -55,9 +52,12 @@ class ControllerTest extends AnyFunSuiteLike: .withRows(peopleComponents.map(p => Seq(p.view))) MV(peopleComponents.map(_.model), component) - val people = Seq(Person(10, "person 1"), Person(20, "person 2")) - val all = newController(Seq(OnChange("person-10", "changed p10")), peopleComponent) + val p1 = Person(10, "person 1") + val p2 = Person(20, "person 2") + val people = Seq(p1, p2) + val mv = newController(Seq(OnChange("person-10", "changed p10")), peopleComponent) .render(people) .iterator - .toList - println(all.mkString("\n")) + .lastOption + .get + mv.model should be(Seq(p1.copy(name = "changed p10"), p2)) From 926ad9592325139c298a931332917e6ac1a26d61 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 5 Mar 2024 16:38:05 +0000 Subject: [PATCH 280/313] - --- .../client/components/chakra/ChakraElement.scala | 14 -------------- .../client/components/std/StdElement.scala | 2 -- .../terminal21/client/components/std/StdHttp.scala | 4 +--- 3 files changed, 1 insertion(+), 19 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala index 77a08981..78417392 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala @@ -167,7 +167,6 @@ case class SimpleGrid( case class Editable( key: String = Keys.nextKey, defaultValue: String = "", - valueReceived: Option[String] = None, // use value instead style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, dataStore: TypedMap = TypedMap.empty @@ -179,7 +178,6 @@ case class Editable( override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withDefaultValue(v: String) = copy(defaultValue = v) - def value = valueReceived.getOrElse(defaultValue) override def withDataStore(ds: TypedMap): Editable = copy(dataStore = ds) case class EditablePreview(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) extends ChakraElement: @@ -260,7 +258,6 @@ case class Input( size: String = "md", variant: Option[String] = None, defaultValue: String = "", - valueReceived: Option[String] = None, // use value instead style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty ) extends ChakraElement @@ -273,7 +270,6 @@ case class Input( def withSize(v: String): Input = copy(size = v) def withVariant(v: Option[String]): Input = copy(variant = v) def withDefaultValue(v: String): Input = copy(defaultValue = v) - def value: String = valueReceived.getOrElse(defaultValue) override def withDataStore(ds: TypedMap): Input = copy(dataStore = ds) case class InputGroup( @@ -329,12 +325,10 @@ case class Checkbox( defaultChecked: Boolean = false, isDisabled: Boolean = false, style: Map[String, Any] = Map.empty, - checkedV: Option[Boolean] = None, dataStore: TypedMap = TypedMap.empty ) extends ChakraElement with OnChangeBooleanEventHandler.CanHandleOnChangeEvent: type This = Checkbox - def checked: Boolean = checkedV.getOrElse(defaultChecked) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) @@ -363,7 +357,6 @@ case class Radio( case class RadioGroup( key: String = Keys.nextKey, defaultValue: String = "", - valueReceived: Option[String] = None, // use value instead style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, dataStore: TypedMap = TypedMap.empty @@ -373,7 +366,6 @@ case class RadioGroup( type This = RadioGroup override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) - def value: String = valueReceived.getOrElse(defaultValue) def withKey(v: String) = copy(key = v) def withDefaultValue(v: String) = copy(defaultValue = v) override def withDataStore(ds: TypedMap): RadioGroup = copy(dataStore = ds) @@ -1618,7 +1610,6 @@ case class Textarea( size: String = "md", variant: Option[String] = None, defaultValue: String = "", - valueReceived: Option[String] = None, // use value instead style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty ) extends ChakraElement @@ -1631,7 +1622,6 @@ case class Textarea( def withSize(v: String) = copy(size = v) def withVariant(v: Option[String]) = copy(variant = v) def withDefaultValue(v: String) = copy(defaultValue = v) - def value = valueReceived.getOrElse(defaultValue) override def withDataStore(ds: TypedMap): Textarea = copy(dataStore = ds) /** https://chakra-ui.com/docs/components/switch @@ -1642,12 +1632,10 @@ case class Switch( defaultChecked: Boolean = false, isDisabled: Boolean = false, style: Map[String, Any] = Map.empty, - checkedV: Option[Boolean] = None, // use checked dataStore: TypedMap = TypedMap.empty ) extends ChakraElement with OnChangeBooleanEventHandler.CanHandleOnChangeEvent: type This = Switch - def checked: Boolean = checkedV.getOrElse(defaultChecked) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) @@ -1661,7 +1649,6 @@ case class Select( key: String = Keys.nextKey, placeholder: String = "", defaultValue: String = "", - valueReceived: Option[String] = None, // use value instead bg: Option[String] = None, color: Option[String] = None, borderColor: Option[String] = None, @@ -1680,7 +1667,6 @@ case class Select( def withBg(v: Option[String]) = copy(bg = v) def withColor(v: Option[String]) = copy(color = v) def withBorderColor(v: Option[String]) = copy(borderColor = v) - def value = valueReceived.getOrElse(defaultValue) override def withDataStore(ds: TypedMap): Select = copy(dataStore = ds) case class Option_( diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala index 3516c03e..d4e357d3 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala @@ -90,7 +90,6 @@ case class Input( `type`: String = "text", defaultValue: String = "", style: Map[String, Any] = Map.empty, - valueReceived: Option[String] = None, // use value instead dataStore: TypedMap = TypedMap.empty ) extends StdElement with CanHandleOnChangeEvent: @@ -99,5 +98,4 @@ case class Input( def withKey(v: String) = copy(key = v) def withType(v: String) = copy(`type` = v) def withDefaultValue(v: String) = copy(defaultValue = v) - def value = valueReceived.getOrElse(defaultValue) override def withDataStore(ds: TypedMap): Input = copy(dataStore = ds) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala index bc3cb0d8..ed13b590 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala @@ -40,12 +40,10 @@ case class Cookie( case class CookieReader( key: String = Keys.nextKey, name: String = "cookie.name", - value: Option[String] = None, // will be set when/if cookie value is read requestId: String = "cookie-read-req", dataStore: TypedMap = TypedMap.empty ) extends StdHttp with CanHandleOnChangeEvent: type This = CookieReader override def withDataStore(ds: TypedMap): CookieReader = copy(dataStore = ds) - - override def withKey(key: String): CookieReader = copy(key = key) + override def withKey(key: String): CookieReader = copy(key = key) From b4db4607759364ac15297cf70d51ce63abec6bbe Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 5 Mar 2024 16:47:41 +0000 Subject: [PATCH 281/313] - --- example-scripts/csv-editor.sc | 10 ++++----- .../org/terminal21/client/Controller.scala | 21 ++++++++++++++----- .../terminal21/client/ControllerTest.scala | 2 +- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/example-scripts/csv-editor.sc b/example-scripts/csv-editor.sc index 154c07d9..41de6d3a 100755 --- a/example-scripts/csv-editor.sc +++ b/example-scripts/csv-editor.sc @@ -52,8 +52,8 @@ def toCsvModel(csv: Seq[Seq[String]]) = class CsvEditor(initModel: CsvModel)(using session: ConnectedSession): - val saveAndExit = Button(key = "save-exit", text = "Save & Exit") - val exit = Button(key = "exit", text = "Exit Without Saving") + val saveAndExit = Button("save-exit", text = "Save & Exit") + val exit = Button("exit", text = "Exit Without Saving") def run(): Unit = for mv <- controller.render(initModel).iterator.lastOption.filter(_.model.save) // only save if model.save is true @@ -72,7 +72,7 @@ class CsvEditor(initModel: CsvModel)(using session: ConnectedSession): model.copy(csv = model.csv + (coords -> newValue), status = s"Changed value at $coords to $newValue") case None => model - val view = QuickTable(key = "csv-editor", variant = "striped", colorScheme = "teal", size = "mg") + val view = QuickTable("csv-editor", variant = "striped", colorScheme = "teal", size = "mg") .withCaption("Please edit the csv contents above and click save to save and exit") .withRows(tableCells) @@ -94,7 +94,7 @@ class CsvEditor(initModel: CsvModel)(using session: ConnectedSession): MV( newModel, view, - isTerminate = newModel.exitWithoutSave || newModel.save + terminate = newModel.exitWithoutSave || newModel.save ) def controller: Controller[CsvModel] = Controller(components) @@ -108,7 +108,7 @@ class CsvEditor(initModel: CsvModel)(using session: ConnectedSession): object CoordsKey extends TypedMapKey[(Int, Int)] private def newEditable(x: Int, y: Int, value: String): Editable = - Editable(key = s"cell-$x-$y", defaultValue = value) + Editable(s"cell-$x-$y", defaultValue = value) .withChildren( EditablePreview(), EditableInput() diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 767bdd2a..5c219e36 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -1,7 +1,8 @@ package org.terminal21.client import org.terminal21.client.collections.EventIterator -import org.terminal21.client.components.UiElement +import org.terminal21.client.components.{OnChangeBooleanEventHandler, OnChangeEventHandler, UiElement} +import org.terminal21.client.components.OnClickEventHandler.CanHandleOnClickEvent import org.terminal21.model.{ClientEvent, CommandEvent, OnChange, OnClick} type ModelViewMaterialized[M] = (M, Events) => MV[M] @@ -56,13 +57,23 @@ case class Events(event: CommandEvent): case OnClick(key) => key == e.key case _ => false - def ifClicked[V](e: UiElement, value: => V): Option[V] = if isClicked(e) then Some(value) else None + def ifClicked[V](e: UiElement & CanHandleOnClickEvent, value: => V): Option[V] = if isClicked(e) then Some(value) else None - def changedValue(e: UiElement, default: String): String = changedValue(e).getOrElse(default) - def changedValue(e: UiElement): Option[String] = event match + def changedValue(e: UiElement & OnChangeEventHandler.CanHandleOnChangeEvent, default: String): String = changedValue(e).getOrElse(default) + def changedValue(e: UiElement & OnChangeEventHandler.CanHandleOnChangeEvent): Option[String] = event match case OnChange(key, value) if key == e.key => Some(value) case _ => None - def isChangedValue(e: UiElement): Boolean = + def isChangedValue(e: UiElement & OnChangeEventHandler.CanHandleOnChangeEvent): Boolean = + event match + case OnChange(key, _) => key == e.key + case _ => false + + def changedBooleanValue(e: UiElement & OnChangeBooleanEventHandler.CanHandleOnChangeEvent, default: Boolean): Boolean = + changedBooleanValue(e).getOrElse(default) + def changedBooleanValue(e: UiElement & OnChangeBooleanEventHandler.CanHandleOnChangeEvent): Option[Boolean] = event match + case OnChange(key, value) if key == e.key => Some(value.toBoolean) + case _ => None + def isChangedBooleanValue(e: UiElement & OnChangeBooleanEventHandler.CanHandleOnChangeEvent): Boolean = event match case OnChange(key, _) => key == e.key case _ => false diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 6a0c4fa3..eae6feba 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -41,7 +41,7 @@ class ControllerTest extends AnyFunSuiteLike: ) MV( person.copy( - name = events.changedValue(nameInput).getOrElse(person.name) + name = events.changedValue(nameInput, person.name) ), component ) From ca04d05bb03521654669f4ef3aadbd70e0f53a84 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 5 Mar 2024 17:06:46 +0000 Subject: [PATCH 282/313] - --- example-scripts/csv-editor.sc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example-scripts/csv-editor.sc b/example-scripts/csv-editor.sc index 41de6d3a..af21ef8b 100755 --- a/example-scripts/csv-editor.sc +++ b/example-scripts/csv-editor.sc @@ -59,7 +59,7 @@ class CsvEditor(initModel: CsvModel)(using session: ConnectedSession): for mv <- controller.render(initModel).iterator.lastOption.filter(_.model.save) // only save if model.save is true do save(mv.model) - def cellsComponent(model: CsvModel, events: Events): MV[CsvModel] = + def editorComponent(model: CsvModel, events: Events): MV[CsvModel] = val tableCells = (0 until model.maxY).map: y => (0 until model.maxX).map: x => @@ -79,7 +79,7 @@ class CsvEditor(initModel: CsvModel)(using session: ConnectedSession): MV(newModel, view) def components(model: CsvModel, events: Events): MV[CsvModel] = - val cells = cellsComponent(model, events) + val cells = editorComponent(model, events) val newModel = cells.model.copy( save = events.isClicked(saveAndExit), exitWithoutSave = events.isClicked(exit) From a07d7d548c5843993c8b29ee0864b807b2a9c495 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 5 Mar 2024 19:02:28 +0000 Subject: [PATCH 283/313] - --- example-scripts/csv-viewer.sc | 4 +- example-scripts/nivo-bar-chart.sc | 75 +++++++++++++++---------------- 2 files changed, 37 insertions(+), 42 deletions(-) diff --git a/example-scripts/csv-viewer.sc b/example-scripts/csv-viewer.sc index b14c61cf..89f760c0 100755 --- a/example-scripts/csv-viewer.sc +++ b/example-scripts/csv-viewer.sc @@ -13,8 +13,6 @@ import org.apache.commons.io.FileUtils import org.terminal21.client.components.chakra.* import java.io.File -import java.util.concurrent.CountDownLatch -import scala.collection.concurrent.TrieMap if args.length != 1 then throw new IllegalArgumentException( @@ -34,7 +32,7 @@ Sessions Controller .noModel( - TableContainer() + TableContainer() // We could use the QuickTable component here, but lets do it a bit more low level with the Chakra components .withChildren( Table(variant = "striped", colorScheme = Some("teal"), size = "mg") .withChildren( diff --git a/example-scripts/nivo-bar-chart.sc b/example-scripts/nivo-bar-chart.sc index 6be19f73..6b7796a0 100755 --- a/example-scripts/nivo-bar-chart.sc +++ b/example-scripts/nivo-bar-chart.sc @@ -9,9 +9,6 @@ import org.terminal21.client.components.nivo.* import scala.util.Random import NivoBarChart.* import org.terminal21.model.ClientEvent -// We don't have a model in this simple example, so we will import the standard Unit model -// for our controller to use. -import org.terminal21.client.Model.Standard.unitModel Sessions .withNewSession("nivo-bar-chart", "Nivo Bar Chart") @@ -19,50 +16,50 @@ Sessions .connect: session => given ConnectedSession = session - val chart = ResponsiveBar( - data = createRandomData, - keys = Seq("hot dog", "burger", "sandwich", "kebab", "fries", "donut"), - indexBy = "country", - padding = 0.3, - defs = Seq( - Defs("dots", "patternDots", "inherit", "#38bcb2", size = Some(4), padding = Some(1), stagger = Some(true)), - Defs("lines", "patternLines", "inherit", "#eed312", rotation = Some(-45), lineWidth = Some(6), spacing = Some(10)) - ), - fill = Seq(Fill("dots", Match("fries")), Fill("lines", Match("sandwich"))), - axisLeft = Some(Axis(legend = "food", legendOffset = -40)), - axisBottom = Some(Axis(legend = "country", legendOffset = 32)), - valueScale = Scale(`type` = "linear"), - indexScale = Scale(`type` = "band", round = Some(true)), - legends = Seq( - Legend( - dataFrom = "keys", - translateX = 120, - itemsSpacing = 2, - itemWidth = 100, - itemHeight = 20, - symbolSize = 20, - symbolShape = "square" + def components(events: Events): Seq[UiElement] = + val data = createRandomData + val chart = ResponsiveBar( + data = data, + keys = Seq("hot dog", "burger", "sandwich", "kebab", "fries", "donut"), + indexBy = "country", + padding = 0.3, + defs = Seq( + Defs("dots", "patternDots", "inherit", "#38bcb2", size = Some(4), padding = Some(1), stagger = Some(true)), + Defs("lines", "patternLines", "inherit", "#eed312", rotation = Some(-45), lineWidth = Some(6), spacing = Some(10)) + ), + fill = Seq(Fill("dots", Match("fries")), Fill("lines", Match("sandwich"))), + axisLeft = Some(Axis(legend = "food", legendOffset = -40)), + axisBottom = Some(Axis(legend = "country", legendOffset = 32)), + valueScale = Scale(`type` = "linear"), + indexScale = Scale(`type` = "band", round = Some(true)), + legends = Seq( + Legend( + dataFrom = "keys", + translateX = 120, + itemsSpacing = 2, + itemWidth = 100, + itemHeight = 20, + symbolSize = 20, + symbolShape = "square" + ) ) ) - ) + Seq( + Paragraph(text = "Various foods.", style = Map("margin" -> 20)), + chart + ) // we'll send new data to our controller every 2 seconds via a custom event - case class NewData(data: Seq[Seq[BarDatum]]) extends ClientEvent + case object Ticker extends ClientEvent fiberExecutor.submit: while !session.isClosed do Thread.sleep(2000) - session.fireEvent(NewData(createRandomData)) + session.fireEvent(Ticker) - Controller( - Seq( - Paragraph(text = "Various foods.", style = Map("margin" -> 20)), - chart - ) - ).render() - .onEvent: // receive the new data and render them - case ControllerClientEvent(handled, NewData(data)) => - handled.withRenderChanges(chart.withData(data)) - .handledEventsIterator + Controller + .noModel(components) + .render() + .iterator .lastOption object NivoBarChart: From 8b2164d249889371dd4feb55f517419589c4c1dd Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 5 Mar 2024 19:47:13 +0000 Subject: [PATCH 284/313] - --- .../{on-change.sc => mvc-user-form.sc} | 50 ++++++++++--------- example-scripts/nivo-line-chart.sc | 41 +++++++-------- 2 files changed, 45 insertions(+), 46 deletions(-) rename example-scripts/{on-change.sc => mvc-user-form.sc} (59%) diff --git a/example-scripts/on-change.sc b/example-scripts/mvc-user-form.sc similarity index 59% rename from example-scripts/on-change.sc rename to example-scripts/mvc-user-form.sc index 8628da6b..1d001466 100755 --- a/example-scripts/on-change.sc +++ b/example-scripts/mvc-user-form.sc @@ -24,39 +24,43 @@ Sessions * page for the user form to be displayed. All components are in `components` method. The controller is in the `controller` method and we can run to get the * result in the `run` method. We can use these methods in unit tests to test what is rendered and how events are processed respectively. */ -class UserPage(user: UserForm)(using ConnectedSession): - given Model[UserForm] = Model(user) // the Model for our page. This is given so that we can handle events and create the controller. +class UserPage(initialForm: UserForm)(using ConnectedSession): /** Runs the form and returns the results * @return * if None, the user didn't submit the form (i.e. closed the session), if Some(userForm) the user submitted the form. */ def run: Option[UserForm] = - controller.render().handledEventsIterator.lastOption.map(_.model).filter(_.submitted) + controller.render(initialForm).iterator.lastOption.map(_.model).filter(_.submitted) /** @return * all the components that should be rendered for the page */ - def components: Seq[UiElement] = - val output = Paragraph(text = "Please modify the email.") - val email = Input(`type` = "email", defaultValue = user.email).onChange: event => - import event.* - val v = event.newValue - handled.withModel(model.copy(email = v)).withRenderChanges(output.withText(s"Email value : $v")) - - Seq( - QuickFormControl() - .withLabel("Email address") - .withInputGroup( - InputLeftAddon().withChildren(EmailIcon()), - email - ) - .withHelperText("We'll never share your email."), - Button(text = "Submit").onClick: event => - import event.* - handled.withModel(model.copy(submitted = true)).terminate // mark the form as submitted and terminate the event iteration - , - output + def components(form: UserForm, events: Events): MV[UserForm] = + val email = Input(key = "email", `type` = "email", defaultValue = initialForm.email) + val submit = Button(key = "submit", text = "Submit") + + val updatedForm = form.copy( + email = events.changedValue(email, form.email), + submitted = events.isClicked(submit) + ) + + val output = Paragraph(text = if events.isChangedValue(email) then s"Email changed: ${updatedForm.email}" else "Please modify the email.") + + MV( + updatedForm, + Seq( + QuickFormControl() + .withLabel("Email address") + .withInputGroup( + InputLeftAddon().withChildren(EmailIcon()), + email + ) + .withHelperText("We'll never share your email."), + submit, + output + ), + terminate = events.isClicked(submit) ) def controller: Controller[UserForm] = Controller(components) diff --git a/example-scripts/nivo-line-chart.sc b/example-scripts/nivo-line-chart.sc index 963b0bee..1acb211d 100755 --- a/example-scripts/nivo-line-chart.sc +++ b/example-scripts/nivo-line-chart.sc @@ -9,9 +9,6 @@ import org.terminal21.client.components.nivo.* import scala.util.Random import NivoLineChart.* import org.terminal21.model.ClientEvent -// We don't have a model in this simple example, so we will import the standard Unit model -// for our controller to use. -import org.terminal21.client.Model.Standard.unitModel Sessions .withNewSession("nivo-line-chart", "Nivo Line Chart") @@ -19,31 +16,29 @@ Sessions .connect: session => given ConnectedSession = session - val chart = ResponsiveLine( - data = createRandomData, - yScale = Scale(stacked = Some(true)), - axisBottom = Some(Axis(legend = "transportation", legendOffset = 36)), - axisLeft = Some(Axis(legend = "count", legendOffset = -40)), - legends = Seq(Legend()) - ) - + def components(events: Events): Seq[UiElement] = + val chart = ResponsiveLine( + data = createRandomData, + yScale = Scale(stacked = Some(true)), + axisBottom = Some(Axis(legend = "transportation", legendOffset = 36)), + axisLeft = Some(Axis(legend = "count", legendOffset = -40)), + legends = Seq(Legend()) + ) + Seq( + Paragraph(text = "Means of transportation for various countries", style = Map("margin" -> 20)), + chart + ) // we'll send new data to our controller every 2 seconds via a custom event - case class NewData(data: Seq[Serie]) extends ClientEvent + case object Ticker extends ClientEvent fiberExecutor.submit: while !session.isClosed do Thread.sleep(2000) - session.fireEvent(NewData(createRandomData)) + session.fireEvent(Ticker) - Controller( - Seq( - Paragraph(text = "Means of transportation for various countries", style = Map("margin" -> 20)), - chart - ) - ).render() - .onEvent: // receive the new data and render them - case ControllerClientEvent(handled, NewData(data)) => - handled.withRenderChanges(chart.withData(data)) - .handledEventsIterator + Controller + .noModel(components) + .render() + .iterator .lastOption object NivoLineChart: From 04389263445047534b6f56c0409a3093627d2b6c Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 5 Mar 2024 19:53:08 +0000 Subject: [PATCH 285/313] - --- .../{on-click.sc => mvc-click-form.sc} | 27 ++++++++++--------- example-scripts/mvc-user-form.sc | 2 +- 2 files changed, 16 insertions(+), 13 deletions(-) rename example-scripts/{on-click.sc => mvc-click-form.sc} (66%) diff --git a/example-scripts/on-click.sc b/example-scripts/mvc-click-form.sc similarity index 66% rename from example-scripts/on-click.sc rename to example-scripts/mvc-click-form.sc index a67cc501..78bf9936 100755 --- a/example-scripts/on-click.sc +++ b/example-scripts/mvc-click-form.sc @@ -9,7 +9,7 @@ import org.terminal21.model.SessionOptions case class ClickForm(clicked: Boolean) Sessions - .withNewSession("on-click-example", "On Click Handler") + .withNewSession("mvc-click-form", "MVC form with a button") .connect: session => given ConnectedSession = session new ClickPage(ClickForm(false)).run match @@ -22,16 +22,19 @@ Sessions * page for the click form to be displayed. All components are in `components` method. The controller is in the `controller` method and we can run to get the * result in the `run` method. We can use these methods in unit tests to test what is rendered and how events are processed respectively. */ -class ClickPage(clickForm: ClickForm)(using ConnectedSession): - given Model[ClickForm] = Model(clickForm) - - def run = controller.render().handledEventsIterator.lastOption.map(_.model) - - def components: Seq[UiElement] = - val msg = Paragraph(text = "Waiting for user to click the button") - val button = Button(text = "Please click me").onClick: event => - import event.* - handled.withModel(model.copy(clicked = true)).withRenderChanges(msg.withText("Button clicked")).terminate - Seq(msg, button) +class ClickPage(initialForm: ClickForm)(using ConnectedSession): + def run = controller.render(initialForm).iterator.lastOption.map(_.model) + + def components(form: ClickForm, events: Events): MV[ClickForm] = + val button = Button(key = "click-me", text = "Please click me") + val updatedForm = form.copy( + clicked = events.isClicked(button) + ) + val msg = Paragraph(text = if updatedForm.clicked then "Button clicked!" else "Waiting for user to click the button") + + MV( + updatedForm, + Seq(msg, button) + ) def controller: Controller[ClickForm] = Controller(components) diff --git a/example-scripts/mvc-user-form.sc b/example-scripts/mvc-user-form.sc index 1d001466..dcd07c8a 100755 --- a/example-scripts/mvc-user-form.sc +++ b/example-scripts/mvc-user-form.sc @@ -11,7 +11,7 @@ case class UserForm( ) Sessions - .withNewSession("on-change-example", "On Change event handler") + .withNewSession("mvc-user-form", "MVC example with a user form") .connect: session => given ConnectedSession = session new UserPage(UserForm("my@email.com", false)).run match From 523373e024cfb08bff8ff90e73e6958984619259 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 5 Mar 2024 22:02:59 +0000 Subject: [PATCH 286/313] - --- example-scripts/postit.sc | 62 ++++++++++++++++++++----------------- example-scripts/progress.sc | 39 +++++++++++++---------- 2 files changed, 55 insertions(+), 46 deletions(-) diff --git a/example-scripts/postit.sc b/example-scripts/postit.sc index a3059279..e371fd84 100755 --- a/example-scripts/postit.sc +++ b/example-scripts/postit.sc @@ -20,37 +20,41 @@ Sessions new PostItPage().run() class PostItPage(using ConnectedSession): - import org.terminal21.client.Model.Standard.unitModel // we won't use a model for this one - def run(): Unit = controller.render().handledEventsIterator.lastOption + case class PostIt(message: String = "", messages: List[String] = Nil) + def run(): Unit = controller.render(PostIt()).iterator.lastOption - def components: Seq[UiElement] = - val editor = Textarea(placeholder = "Please post your note by clicking here and editing the content") - val messages = VStack(align = Some("stretch")) - val add = Button(text = "Post It.").onClick: event => - import event.* - // add the new msg. - // note: editor.value is automatically updated by terminal-ui - val currentMessages = messages.current - handled.withRenderChanges( - currentMessages - .addChildren( - HStack().withChildren( - Image( - src = "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fa/Apple_Notes_icon.svg/2048px-Apple_Notes_icon.svg.png", - boxSize = Some("32px") - ), - Box(text = editor.current.value) - ) - ) + def components(model: PostIt, events: Events): MV[PostIt] = + val editor = Textarea("postit-message", placeholder = "Please post your note by clicking here and editing the content") + val addButton = Button("postit", text = "Post It.") + + val updatedMessages = model.messages ++ events.ifClicked(addButton, model.message) + val updatedModel = model.copy( + message = events.changedValue(editor, model.message), + messages = updatedMessages + ) + + val messagesVStack = VStack( + align = Some("stretch"), + children = updatedMessages.map: msg => + HStack().withChildren( + Image( + src = "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fa/Apple_Notes_icon.svg/2048px-Apple_Notes_icon.svg.png", + boxSize = Some("32px") + ), + Box(text = msg) + ) + ) + MV( + updatedModel, + Seq( + Paragraph(text = "Please type your note below and click 'Post It' to post it so that everyone can view it."), + InputGroup().withChildren( + InputLeftAddon().withChildren(EditIcon()), + editor + ), + addButton, + messagesVStack ) - Seq( - Paragraph(text = "Please type your note below and click 'Post It' to post it so that everyone can view it."), - InputGroup().withChildren( - InputLeftAddon().withChildren(EditIcon()), - editor - ), - add, - messages ) def controller = Controller(components) diff --git a/example-scripts/progress.sc b/example-scripts/progress.sc index 1bd6456d..95fea30e 100755 --- a/example-scripts/progress.sc +++ b/example-scripts/progress.sc @@ -1,10 +1,10 @@ #!/usr/bin/env -S scala-cli project.scala -import org.terminal21.client.* +import org.terminal21.client.{*, given} import org.terminal21.client.components.* import org.terminal21.client.components.std.* import org.terminal21.client.components.chakra.* -import org.terminal21.model.SessionOptions +import org.terminal21.model.{ClientEvent, SessionOptions} Sessions .withNewSession("universe-generation", "Universe Generation Progress") @@ -12,23 +12,28 @@ Sessions .connect: session => given ConnectedSession = session - val msg = Paragraph(text = "Generating universe ...") - val progress = Progress(value = 1) + def components(model: Int, events: Events): MV[Int] = + val status = + if model < 10 then "Generating universe ..." + else if model < 30 then "Creating atoms" + else if model < 50 then "Big bang!" + else if model < 80 then "Inflating" + else "Life evolution" - Seq(msg, progress).render() + val msg = Paragraph(text = status) + val progress = Progress(value = model) - for i <- 1 to 100 do - val p = progress.withValue(i) - val m = - if i < 10 then msg - else if i < 30 then msg.withText("Creating atoms") - else if i < 50 then msg.withText("Big bang!") - else if i < 80 then msg.withText("Inflating") - else msg.withText("Life evolution") + MV( + model + 1, + Seq(msg, progress) + ) - Seq(p, m).renderChanges() - Thread.sleep(100) + object Ticker extends ClientEvent + fiberExecutor.submit: + for _ <- 1 to 100 do + Thread.sleep(200) + session.fireEvent(Ticker) + Controller(components).render(1).iterator.takeWhile(_.model < 100).foreach(_ => ()) // clear UI - session.clear() - Paragraph(text = "Universe ready!").render() + session.render(Seq(Paragraph(text = "Universe ready!"))) From be1fd3adede6bfeb46ddf509f4328d1713e06db5 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 5 Mar 2024 22:18:22 +0000 Subject: [PATCH 287/313] - --- example-scripts/read-changed-value.sc | 32 ---------- example-scripts/textedit.sc | 85 ++++++++++++++------------- 2 files changed, 43 insertions(+), 74 deletions(-) delete mode 100755 example-scripts/read-changed-value.sc diff --git a/example-scripts/read-changed-value.sc b/example-scripts/read-changed-value.sc deleted file mode 100755 index e47778c1..00000000 --- a/example-scripts/read-changed-value.sc +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env -S scala-cli project.scala - -import org.terminal21.client.* -import org.terminal21.client.components.* -import org.terminal21.client.components.std.Paragraph -import org.terminal21.client.components.chakra.* - -Sessions - .withNewSession("read-changed-value-example", "Read Changed Value") - .connect: session => - given ConnectedSession = session - - val email = Input(`type` = "email", defaultValue = "my@email.com") - val output = Box() - - Seq( - FormControl().withChildren( - FormLabel(text = "Email address"), - InputGroup().withChildren( - InputLeftAddon().withChildren(EmailIcon()), - email - ), - FormHelperText(text = "We'll never share your email.") - ), - Button(text = "Read Value").onClick: () => - val value = email.current.value - output.current.addChildren(Paragraph(text = s"The value now is $value")).renderChanges() - , - output - ).render() - - session.waitTillUserClosesSession() diff --git a/example-scripts/textedit.sc b/example-scripts/textedit.sc index 025cd95b..1c8caa6f 100755 --- a/example-scripts/textedit.sc +++ b/example-scripts/textedit.sc @@ -35,52 +35,53 @@ Sessions .withNewSession(s"textedit-$fileName", s"Edit: $fileName") .connect: session => given ConnectedSession = session - // we will wait till the user clicks the "Exit" menu, this latch makes sure the main thread of the app waits. - val exitLatch = new CountDownLatch(1) - // the main editor area. - val editor = Textarea(defaultValue = contents) - // This will display a "saved" badge for a second when the user saves the file - val status = Badge() - // This will display an asterisk when the contents of the file are changed in the editor - val modified = Badge(colorScheme = Some("red")) - - // when the user changes the textarea, we get the new text and we can compare it with the loaded value. - editor.onChange: newValue => - modified.withText(if newValue != contents then "*" else "").renderChanges() - Seq( - HStack().withChildren( - Menu().withChildren( - MenuButton(text = "File").withChildren(ChevronDownIcon()), - MenuList().withChildren( - MenuItem(text = "Save") - .onClick: () => - saveFile(editor.current.value) - // we'll display a "Saved" badge for 1 second. - Seq( - status.withText("Saved"), - modified.withText("") - ).renderChanges() - // each event handler runs on a new fiber, it is ok to sleep here - Thread.sleep(1000) - status.withText("").renderChanges() - , - MenuItem(text = "Exit") - .onClick: () => - exitLatch.countDown() - ) + case class Edit(content: String, save: Boolean) + // the main editor area. + def components(edit: Edit, events: Events) = + val editorTextArea = Textarea(key = "editor", defaultValue = edit.content) + // This will display a "saved" badge for a second when the user saves the file + val status = Badge() + // This will display an asterisk when the contents of the file are changed in the editor + val modified = Badge(colorScheme = Some("red"), text = if edit.content != contents then "*" else "") + val saveMenu = MenuItem("save-menu", text = "Save") + val exitMenu = MenuItem("exit-menu", text = "Exit") + val updatedEditor = edit.copy( + content = events.changedValue(editorTextArea, edit.content) + ) + Seq( + HStack().withChildren( + Menu().withChildren( + MenuButton("file-menu", text = "File").withChildren(ChevronDownIcon()), + MenuList().withChildren( + saveMenu + .onClick: () => + saveFile(edit.current.value) + // we'll display a "Saved" badge for 1 second. + Seq( + status.withText("Saved"), + modified.withText("") + ).renderChanges() + // each event handler runs on a new fiber, it is ok to sleep here + Thread.sleep(1000) + status.withText("").renderChanges() + , + exitMenu + .onClick: () => + exitLatch.countDown() + ) + ), + status, + modified ), - status, - modified - ), - FormControl().withChildren( - FormLabel(text = "Editor"), - InputGroup().withChildren( - InputLeftAddon().withChildren(EditIcon()), - editor + FormControl().withChildren( + FormLabel(text = "Editor"), + InputGroup().withChildren( + InputLeftAddon().withChildren(EditIcon()), + edit + ) ) ) - ).render() println(s"Now open ${session.uiUrl} to view the UI") exitLatch.await() From c42cedb7f2069b87a930a17b71f91c4019a811a1 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Tue, 5 Mar 2024 23:42:18 +0000 Subject: [PATCH 288/313] - --- example-scripts/textedit.sc | 54 ++++++++++--------- .../org/terminal21/client/Controller.scala | 2 + 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/example-scripts/textedit.sc b/example-scripts/textedit.sc index 1c8caa6f..2741451a 100755 --- a/example-scripts/textedit.sc +++ b/example-scripts/textedit.sc @@ -29,46 +29,41 @@ val file = new File(fileName) val contents = if file.exists() then FileUtils.readFileToString(file, "UTF-8") else "" -def saveFile(content: String) = FileUtils.writeStringToFile(file, content, "UTF-8") +def saveFile(content: String) = + println(s"Saving file $fileName") + FileUtils.writeStringToFile(file, content, "UTF-8") Sessions .withNewSession(s"textedit-$fileName", s"Edit: $fileName") .connect: session => given ConnectedSession = session - case class Edit(content: String, save: Boolean) + case class Edit(content: String, savedContent: String, save: Boolean, exit: Boolean) // the main editor area. - def components(edit: Edit, events: Events) = + def components(edit: Edit, events: Events): MV[Edit] = val editorTextArea = Textarea(key = "editor", defaultValue = edit.content) // This will display a "saved" badge for a second when the user saves the file - val status = Badge() // This will display an asterisk when the contents of the file are changed in the editor - val modified = Badge(colorScheme = Some("red"), text = if edit.content != contents then "*" else "") val saveMenu = MenuItem("save-menu", text = "Save") val exitMenu = MenuItem("exit-menu", text = "Exit") - val updatedEditor = edit.copy( - content = events.changedValue(editorTextArea, edit.content) + val isSave = events.isClicked(saveMenu) + val updatedContent = events.changedValue(editorTextArea, edit.content) + val updatedEdit = edit.copy( + content = updatedContent, + save = isSave, + savedContent = if isSave then updatedContent else edit.savedContent, + exit = events.isClicked(exitMenu) ) - Seq( + val modified = Badge(colorScheme = Some("red"), text = if updatedEdit.content != updatedEdit.savedContent then "*" else "") + val status = Badge(text = if updatedEdit.save then "Saved" else "") + + val view = Seq( HStack().withChildren( Menu().withChildren( MenuButton("file-menu", text = "File").withChildren(ChevronDownIcon()), MenuList().withChildren( - saveMenu - .onClick: () => - saveFile(edit.current.value) - // we'll display a "Saved" badge for 1 second. - Seq( - status.withText("Saved"), - modified.withText("") - ).renderChanges() - // each event handler runs on a new fiber, it is ok to sleep here - Thread.sleep(1000) - status.withText("").renderChanges() - , + saveMenu, exitMenu - .onClick: () => - exitLatch.countDown() ) ), status, @@ -78,12 +73,19 @@ Sessions FormLabel(text = "Editor"), InputGroup().withChildren( InputLeftAddon().withChildren(EditIcon()), - edit + editorTextArea ) ) ) + MV(updatedEdit, view) + println(s"Now open ${session.uiUrl} to view the UI") - exitLatch.await() - session.clear() - Paragraph(text = "Terminated").render() + Controller(components) + .render(Edit(contents, contents, false, false)) + .iterator + .tapEach: mv => + if mv.model.save then saveFile(mv.model.content) + .takeWhile(!_.model.exit) + .foreach(_ => ()) + session.render(Seq(Paragraph(text = "Terminated"))) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 5c219e36..c4a83ed4 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -52,6 +52,8 @@ class RenderedController[M]( .takeWhile(!_.terminate) ) + def run(): Option[M] = iterator.lastOption.map(_.model) + case class Events(event: CommandEvent): def isClicked(e: UiElement): Boolean = event match case OnClick(key) => key == e.key From 9a51bba978435652e92e3268e1ea20ccea7b104a Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 6 Mar 2024 15:09:20 +0000 Subject: [PATCH 289/313] - --- .../src/main/scala/tests/chakra/Forms.scala | 6 +- .../client/components/mathjax/MathJax.scala | 2 +- .../client/components/nivo/NivoElement.scala | 4 +- .../org/terminal21/collections/TypedMap.scala | 2 +- .../terminal21/collections/TypedMapTest.scala | 30 +-- .../sparklib/CalculationsExtensions.scala | 17 +- .../calculations/SparkCalculation.scala | 64 +++-- .../StdUiSparkCalculationTest.scala | 103 -------- .../sparklib/endtoend/SparkBasics.scala | 76 +++--- .../client/components/CachedCalculation.scala | 11 - .../client/components/Calculation.scala | 33 --- .../client/components/StdUiCalculation.scala | 88 ------- .../components/chakra/ChakraElement.scala | 242 +++++++++--------- .../components/chakra/QuickFormControl.scala | 2 +- .../client/components/chakra/QuickTable.scala | 2 +- .../client/components/chakra/QuickTabs.scala | 2 +- .../components/frontend/FrontEndElement.scala | 2 +- .../client/components/std/StdElement.scala | 22 +- .../client/components/std/StdHttp.scala | 4 +- .../terminal21/client/CalculationTest.scala | 37 --- 20 files changed, 256 insertions(+), 493 deletions(-) delete mode 100644 terminal21-spark/src/test/scala/org/terminal21/sparklib/calculations/StdUiSparkCalculationTest.scala delete mode 100644 terminal21-ui-std/src/main/scala/org/terminal21/client/components/CachedCalculation.scala delete mode 100644 terminal21-ui-std/src/main/scala/org/terminal21/client/components/Calculation.scala delete mode 100644 terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala delete mode 100644 terminal21-ui-std/src/test/scala/org/terminal21/client/CalculationTest.scala diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala b/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala index 15df5574..8e60e550 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala @@ -56,11 +56,11 @@ object Forms: .map(v => s"dob = $v") ++ events .changedValue(color) .map(v => s"color = $v") ++ events - .changedValue(checkbox2) + .changedBooleanValue(checkbox2) .map(v => s"checkbox2 checked is $v") ++ events - .changedValue(checkbox1) + .changedBooleanValue(checkbox1) .map(v => s"checkbox1 checked is $v") ++ events - .changedValue(switch1) + .changedBooleanValue(switch1) .map(v => s"switch1 checked is $v") ++ events .changedValue(radioGroup) .map(v => s"radioGroup newValue=$v") ++ events diff --git a/terminal21-mathjax/src/main/scala/org/terminal21/client/components/mathjax/MathJax.scala b/terminal21-mathjax/src/main/scala/org/terminal21/client/components/mathjax/MathJax.scala index a44e0622..c41159da 100644 --- a/terminal21-mathjax/src/main/scala/org/terminal21/client/components/mathjax/MathJax.scala +++ b/terminal21-mathjax/src/main/scala/org/terminal21/client/components/mathjax/MathJax.scala @@ -13,7 +13,7 @@ case class MathJax( // expression should be like """ text \( asciimath \) text""", i.e. """When \(a \ne 0\), there are two solutions to \(ax^2 + bx + c = 0\)""" expression: String = """fill in the expression as per https://asciimath.org/""", style: Map[String, Any] = Map.empty, // Note: some of the styles are ignored by mathjax lib - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends MathJaxElement with HasStyle: type This = MathJax diff --git a/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/NivoElement.scala b/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/NivoElement.scala index fc2d5a26..1782eec5 100644 --- a/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/NivoElement.scala +++ b/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/NivoElement.scala @@ -30,7 +30,7 @@ case class ResponsiveLine( pointLabelYOffset: Int = -12, useMesh: Boolean = true, legends: Seq[Legend] = Nil, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends NivoElement: type This = ResponsiveLine override def withStyle(v: Map[String, Any]): ResponsiveLine = copy(style = v) @@ -61,7 +61,7 @@ case class ResponsiveBar( axisLeft: Option[Axis] = Some(Axis(legend = "x", legendOffset = -40)), legends: Seq[Legend] = Nil, ariaLabel: String = "Chart Label", - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends NivoElement: type This = ResponsiveBar override def withStyle(v: Map[String, Any]): ResponsiveBar = copy(style = v) diff --git a/terminal21-server-client-common/src/main/scala/org/terminal21/collections/TypedMap.scala b/terminal21-server-client-common/src/main/scala/org/terminal21/collections/TypedMap.scala index f60bbb66..52f05908 100644 --- a/terminal21-server-client-common/src/main/scala/org/terminal21/collections/TypedMap.scala +++ b/terminal21-server-client-common/src/main/scala/org/terminal21/collections/TypedMap.scala @@ -20,7 +20,7 @@ class TypedMap(protected val m: TMMap): override def toString = s"TypedMap(${m.keys.mkString(", ")})" object TypedMap: - def empty = new TypedMap(Map.empty) + val Empty = new TypedMap(Map.empty) def apply(kv: (TypedMapKey[_], Any)*) = val m = Map(kv*) new TypedMap(m) diff --git a/terminal21-server-client-common/src/test/scala/org/terminal21/collections/TypedMapTest.scala b/terminal21-server-client-common/src/test/scala/org/terminal21/collections/TypedMapTest.scala index 1a961d54..504427c8 100644 --- a/terminal21-server-client-common/src/test/scala/org/terminal21/collections/TypedMapTest.scala +++ b/terminal21-server-client-common/src/test/scala/org/terminal21/collections/TypedMapTest.scala @@ -8,7 +8,7 @@ class TypedMapTest extends AnyFunSuiteLike: object StringKey extends TypedMapKey[String] test("apply"): - val m = TypedMap.empty + (IntKey -> 5) + (StringKey -> "x") + val m = TypedMap.Empty + (IntKey -> 5) + (StringKey -> "x") m(IntKey) should be(5) m(StringKey) should be("x") @@ -22,41 +22,41 @@ class TypedMapTest extends AnyFunSuiteLike: m.keys.toSet should be(Set(IntKey, StringKey)) test("get"): - val m = TypedMap.empty + (IntKey -> 5) + (StringKey -> "x") + val m = TypedMap.Empty + (IntKey -> 5) + (StringKey -> "x") m.get(IntKey) should be(Some(5)) m.get(StringKey) should be(Some("x")) test("getOrElse when key not available"): - TypedMap.empty.getOrElse(IntKey, 2) should be(2) + TypedMap.Empty.getOrElse(IntKey, 2) should be(2) test("getOrElse when key available"): - (TypedMap.empty + (IntKey -> 5)).getOrElse(IntKey, 2) should be(5) + (TypedMap.Empty + (IntKey -> 5)).getOrElse(IntKey, 2) should be(5) test("contains key positive"): - (TypedMap.empty + (IntKey -> 5)).contains(IntKey) should be(true) + (TypedMap.Empty + (IntKey -> 5)).contains(IntKey) should be(true) test("contains key negative"): - TypedMap.empty.contains(IntKey) should be(false) + TypedMap.Empty.contains(IntKey) should be(false) test("get key negative"): - TypedMap.empty.get(IntKey) should be(None) + TypedMap.Empty.get(IntKey) should be(None) test("equals positive"): - val m1 = TypedMap.empty + (IntKey -> 5) - val m2 = TypedMap.empty + (IntKey -> 5) + val m1 = TypedMap.Empty + (IntKey -> 5) + val m2 = TypedMap.Empty + (IntKey -> 5) m1 should be(m2) test("equals negative"): - val m1 = TypedMap.empty + (IntKey -> 5) - val m2 = TypedMap.empty + (IntKey -> 6) + val m1 = TypedMap.Empty + (IntKey -> 5) + val m2 = TypedMap.Empty + (IntKey -> 6) m1 should not be m2 test("hashCode positive"): - val m1 = TypedMap.empty + (IntKey -> 5) - val m2 = TypedMap.empty + (IntKey -> 5) + val m1 = TypedMap.Empty + (IntKey -> 5) + val m2 = TypedMap.Empty + (IntKey -> 5) m1.hashCode should be(m2.hashCode) test("hashCode negative"): - val m1 = TypedMap.empty + (IntKey -> 5) - val m2 = TypedMap.empty + (IntKey -> 6) + val m1 = TypedMap.Empty + (IntKey -> 5) + val m2 = TypedMap.Empty + (IntKey -> 6) m1.hashCode should not be m2.hashCode diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/CalculationsExtensions.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/CalculationsExtensions.scala index a5408c0a..8cfa21b7 100644 --- a/terminal21-spark/src/main/scala/org/terminal21/sparklib/CalculationsExtensions.scala +++ b/terminal21-spark/src/main/scala/org/terminal21/sparklib/CalculationsExtensions.scala @@ -3,9 +3,9 @@ package org.terminal21.sparklib import functions.fibers.FiberExecutor import org.apache.spark.sql.SparkSession import org.terminal21.client.* +import org.terminal21.client.components.UiElement import org.terminal21.client.components.UiElement.HasStyle -import org.terminal21.client.components.{Keys, UiElement} -import org.terminal21.sparklib.calculations.{ReadWriter, StdUiSparkCalculation} +import org.terminal21.sparklib.calculations.{ReadWriter, SparkCalculation} extension [OUT: ReadWriter](ds: OUT) def visualize(name: String, dataUi: UiElement & HasStyle)( @@ -13,14 +13,9 @@ extension [OUT: ReadWriter](ds: OUT) )(using ConnectedSession, FiberExecutor, - SparkSession + SparkSession, + Events ) = - val ui = new StdUiSparkCalculation[OUT](Keys.nextKey, name, dataUi): - override protected def whenResultsReady(results: OUT): Unit = - try updateUi(toUi(results)) - catch case t: Throwable => t.printStackTrace() - super.whenResultsReady(results) - override def nonCachedCalculation: OUT = ds - - ui.run() + val ui = new SparkCalculation[OUT](s"spark-calc-$name", name, dataUi, toUi, ds) + ui.runCalculation() ui diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala index 19eb2bea..be2f8e13 100644 --- a/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala +++ b/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala @@ -1,11 +1,15 @@ package org.terminal21.sparklib.calculations -import functions.fibers.FiberExecutor +import functions.fibers.{Fiber, FiberExecutor} import org.apache.commons.io.FileUtils import org.apache.spark.sql.SparkSession -import org.terminal21.client.* +import org.terminal21.client.{*, given} import org.terminal21.client.components.UiElement.HasStyle -import org.terminal21.client.components.{CachedCalculation, StdUiCalculation, UiComponent, UiElement} +import org.terminal21.client.components.chakra.* +import org.terminal21.client.components.* +import org.terminal21.collections.TypedMap +import org.terminal21.model.ClientEvent +import org.terminal21.sparklib.calculations.SparkCalculation.TriggerRedraw import org.terminal21.sparklib.util.Environment import java.io.File @@ -16,10 +20,23 @@ import java.io.File * * Because the cache is stored in the disk, it is available even if the jvm running the code restarts. This allows the user to run and rerun their code without * having to rerun the spark calculation. - * - * Subclass this to create your own UI for a spark calculation, see StdUiSparkCalculation below. */ -trait SparkCalculation[OUT: ReadWriter](name: String)(using executor: FiberExecutor, spark: SparkSession) extends CachedCalculation[OUT] with UiComponent: +case class SparkCalculation[OUT: ReadWriter]( + key: String, + name: String, + dataUi: UiElement with HasStyle, + toUi: OUT => UiElement & HasStyle, + dataSet: OUT, + dataStore: TypedMap = TypedMap.Empty +)(using + spark: SparkSession, + session: ConnectedSession, + events: Events +) extends UiComponent: + override type This = SparkCalculation[OUT] + override def withKey(key: String): This = copy(key = key) + override def withDataStore(ds: TypedMap): This = copy(dataStore = ds) + private val rw = implicitly[ReadWriter[OUT]] private val rootFolder = s"${Environment.tmpDirectory}/spark-calculations" private val targetDir = s"$rootFolder/$name" @@ -31,7 +48,7 @@ trait SparkCalculation[OUT: ReadWriter](name: String)(using executor: FiberExecu if isCached then reader else writer - override def invalidateCache(): Unit = + def invalidateCache(): Unit = FileUtils.deleteDirectory(new File(targetDir)) private def calculateOnce(f: => OUT): OUT = @@ -43,12 +60,29 @@ trait SparkCalculation[OUT: ReadWriter](name: String)(using executor: FiberExecu } ) - override protected def calculation(): OUT = calculateOnce(nonCachedCalculation) + def runCalculation(): Unit = + if events.event != TriggerRedraw then + fiberExecutor.submit: + println("runCalculation()") + val r = calculateOnce(dataSet) + println("Redraw") + session.fireEvent(TriggerRedraw) + r + val badge = Badge(s"recalc-badge-$name") + val recalc = Button(s"recalc-button-$name", text = "Recalculate", size = Some("sm"), leftIcon = Some(RepeatIcon())) -abstract class StdUiSparkCalculation[OUT: ReadWriter]( - override val key: String, - name: String, - dataUi: UiElement with HasStyle -)(using ConnectedSession, FiberExecutor, SparkSession) - extends StdUiCalculation[OUT](key, name, dataUi) - with SparkCalculation[OUT](name) + override def rendered: Seq[UiElement] = + val header = Box( + s"recalc-box-$name", + bg = "green", + p = 4, + children = Seq( + HStack(children = Seq(Text(text = name), badge, recalc)) + ) + ) + val ui = out.map(toUi).getOrElse(dataUi) + println(ui) + Seq(header, ui) + +object SparkCalculation: + object TriggerRedraw extends ClientEvent diff --git a/terminal21-spark/src/test/scala/org/terminal21/sparklib/calculations/StdUiSparkCalculationTest.scala b/terminal21-spark/src/test/scala/org/terminal21/sparklib/calculations/StdUiSparkCalculationTest.scala deleted file mode 100644 index 3a567b4b..00000000 --- a/terminal21-spark/src/test/scala/org/terminal21/sparklib/calculations/StdUiSparkCalculationTest.scala +++ /dev/null @@ -1,103 +0,0 @@ -package org.terminal21.sparklib.calculations - -import org.apache.spark.sql.{Dataset, Encoder, SparkSession} -import org.scalatest.concurrent.Eventually -import org.scalatest.funsuite.AnyFunSuiteLike -import org.scalatest.matchers.should.Matchers.* -import org.scalatest.time.{Millis, Span} -import org.terminal21.client.components.Keys -import org.terminal21.client.components.chakra.* -import org.terminal21.client.{ConnectedSession, ConnectedSessionMock, Controller, Model, given} -import org.terminal21.sparklib.SparkSessions - -import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger} -import scala.util.Using - -class StdUiSparkCalculationTest extends AnyFunSuiteLike with Eventually: - given PatienceConfig = PatienceConfig(scaled(Span(3000, Millis))) - given Model[Unit] = Model.Standard.unitModel - - test("calculates the correct result"): - Using.resource(SparkSessions.newSparkSession()): spark => - import spark.implicits.* - given ConnectedSession = ConnectedSessionMock.newConnectedSessionMock - given SparkSession = spark - val calc = new TestingCalculation - calc.run().get().collect().toList should be(List(2)) - - test("whenResultsNotReady"): - Using.resource(SparkSessions.newSparkSession()): spark => - import spark.implicits.* - given ConnectedSession = ConnectedSessionMock.newConnectedSessionMock - given SparkSession = spark - val called = new AtomicBoolean(false) - val calc = new TestingCalculation: - override protected def whenResultsNotReady(): Unit = - called.set(true) - calc.run().get() - called.get() should be(true) - - test("whenResultsReady"): - Using.resource(SparkSessions.newSparkSession()): spark => - import spark.implicits.* - given ConnectedSession = ConnectedSessionMock.newConnectedSessionMock - given SparkSession = spark - val called = new AtomicBoolean(false) - val calc = new TestingCalculation: - override protected def whenResultsReady(results: Dataset[Int]): Unit = - results.collect().toList should be(List(2)) - called.set(true) - - calc.run() - eventually: - called.get() should be(true) - - test("whenResultsReady called even when cached"): - Using.resource(SparkSessions.newSparkSession()): spark => - import spark.implicits.* - given ConnectedSession = ConnectedSessionMock.newConnectedSessionMock - given SparkSession = spark - val called = new AtomicInteger(0) - val calc = new TestingCalculation: - override protected def whenResultsReady(results: Dataset[Int]): Unit = - results.collect().toList should be(List(2)) - called.incrementAndGet() - - calc.run().get() - calc.run() - eventually: - called.get() should be(2) - - test("caches results"): - Using.resource(SparkSessions.newSparkSession()): spark => - import spark.implicits.* - given ConnectedSession = ConnectedSessionMock.newConnectedSessionMock - given SparkSession = spark - val calc = new TestingCalculation - calc.run().get().collect().toList should be(List(2)) - calc.run().get().collect().toList should be(List(2)) - calc.calcCalledTimes.get() should be(1) - - test("refresh button invalidates cache and runs calculations"): - Using.resource(SparkSessions.newSparkSession()): spark => - import spark.implicits.* - given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock - given SparkSession = spark - val calc = new TestingCalculation - calc.run().get().collect().toList should be(List(2)) - val it = Controller(Seq(calc)).render().handledEventsIterator - session.fireClickEvent(calc.recalc) - session.fireSessionClosedEvent() - it.lastOption - eventually: - calc.calcCalledTimes.get() should be(2) - -class TestingCalculation(using spark: SparkSession)(using ConnectedSession, Model[_], Encoder[Int]) - extends StdUiSparkCalculation[Dataset[Int]](Keys.nextKey, "testing-calc", Box()): - val calcCalledTimes = new AtomicInteger(0) - invalidateCache() - - override def nonCachedCalculation: Dataset[Int] = - import spark.implicits.* - calcCalledTimes.incrementAndGet() - Seq(2).toDS diff --git a/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala b/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala index 61e55152..54ea3402 100644 --- a/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala +++ b/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala @@ -25,52 +25,58 @@ import scala.util.Using val headers = Seq("id", "name", "path", "numOfLines", "numOfWords", "createdDate", "timestamp") - val sortedFilesTable = QuickTable().withHeaders(headers: _*).caption("Files sorted by createdDate and numOfWords") - val codeFilesTable = QuickTable().withHeaders(headers: _*).caption("Unsorted files") + def components(events: Events) = + println("components() START") + given Events = events + val sortedFilesTable = QuickTable().withHeaders(headers: _*).caption("Files sorted by createdDate and numOfWords") + val codeFilesTable = QuickTable().withHeaders(headers: _*).caption("Unsorted files") - val sortedSourceFilesDS = sortedSourceFiles(sourceFiles()) - val sortedCalc = sortedSourceFilesDS.visualize("Sorted files", sortedFilesTable): results => - val tableRows = results.take(3).toList.map(_.toData) - sortedFilesTable.withRows(tableRows) + val sortedSourceFilesDS = sortedSourceFiles(sourceFiles()) + val sortedCalc = sortedSourceFilesDS.visualize("Sorted files", sortedFilesTable): results => + val tableRows = results.take(3).toList.map(_.toData) + sortedFilesTable.withRows(tableRows) - val codeFilesCalculation = sourceFiles().visualize("Code files", codeFilesTable): results => - val dt = results.take(3).toList - codeFilesTable.withRows(dt.map(_.toData)) + val codeFilesCalculation = sourceFiles().visualize("Code files", codeFilesTable): results => + val dt = results.take(3).toList + codeFilesTable.withRows(dt.map(_.toData)) - val sortedFilesTableDF = QuickTable().withHeaders(headers: _*).caption("Files sorted by createdDate and numOfWords ASC and as DF") - val sortedCalcAsDF = sourceFiles() - .sort($"createdDate".asc, $"numOfWords".asc) - .toDF() - .visualize("Sorted files DF", sortedFilesTableDF): results => - val tableRows = results.take(4).toList - sortedFilesTableDF.withRows(tableRows.toUiTable) + val sortedFilesTableDF = QuickTable().withHeaders(headers: _*).caption("Files sorted by createdDate and numOfWords ASC and as DF") + val sortedCalcAsDF = sourceFiles() + .sort($"createdDate".asc, $"numOfWords".asc) + .toDF() + .visualize("Sorted files DF", sortedFilesTableDF): results => + val tableRows = results.take(4).toList + sortedFilesTableDF.withRows(tableRows.toUiTable) - val chart = ResponsiveLine( - data = Seq( - Serie( - "Scala", - data = Nil - ) - ), - axisBottom = Some(Axis(legend = "Class", legendOffset = 36)), - axisLeft = Some(Axis(legend = "Number of Lines", legendOffset = -40)), - legends = Seq(Legend()) - ) - - val sourceFileChart = sourceFiles() - .sort($"numOfLines".desc) - .visualize("Biggest Code Files", chart): results => - val data = results.take(10).map(cf => Datum(StringUtils.substringBeforeLast(cf.name, ".scala"), cf.numOfLines)).toList - chart.withData(Seq(Serie("Scala", data = data))) + val chart = ResponsiveLine( + data = Seq( + Serie( + "Scala", + data = Nil + ) + ), + axisBottom = Some(Axis(legend = "Class", legendOffset = 36)), + axisLeft = Some(Axis(legend = "Number of Lines", legendOffset = -40)), + legends = Seq(Legend()) + ) - Controller( + val sourceFileChart = sourceFiles() + .sort($"numOfLines".desc) + .visualize("Biggest Code Files", chart): results => + val data = results.take(10).map(cf => Datum(StringUtils.substringBeforeLast(cf.name, ".scala"), cf.numOfLines)).toList + chart.withData(Seq(Serie("Scala", data = data))) + println("components() RETURNING") Seq( codeFilesCalculation, sortedCalc, sortedCalcAsDF, sourceFileChart ) - ).render().handledEventsIterator.lastOption + + Controller + .noModel(components) + .render() + .run() def sourceFiles()(using spark: SparkSession) = import scala3encoders.given diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/CachedCalculation.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/CachedCalculation.scala deleted file mode 100644 index ba1cbe84..00000000 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/CachedCalculation.scala +++ /dev/null @@ -1,11 +0,0 @@ -package org.terminal21.client.components - -import functions.fibers.{Fiber, FiberExecutor} - -trait CachedCalculation[OUT](using executor: FiberExecutor) extends Calculation[OUT]: - def isCached: Boolean - def invalidateCache(): Unit - def nonCachedCalculation: OUT - override def reCalculate(): Fiber[OUT] = - invalidateCache() - super.reCalculate() diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/Calculation.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/Calculation.scala deleted file mode 100644 index 30c933e0..00000000 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/Calculation.scala +++ /dev/null @@ -1,33 +0,0 @@ -package org.terminal21.client.components - -import functions.fibers.{Fiber, FiberExecutor} - -import java.util.concurrent.CountDownLatch - -trait Calculation[OUT](using executor: FiberExecutor): - protected def calculation(): OUT - protected def whenResultsNotReady(): Unit = () - protected def whenResultsReady(results: OUT): Unit = () - - def reCalculate(): Fiber[OUT] = run() - - def onError(t: Throwable): Unit = - t.printStackTrace() - - def run(): Fiber[OUT] = - val refreshInOrder = new CountDownLatch(1) - executor.submit: - try - executor.submit: - try whenResultsNotReady() - finally refreshInOrder.countDown() - - val out = calculation() - refreshInOrder.await() - executor.submit: - whenResultsReady(out) - out - catch - case t: Throwable => - onError(t) - throw t diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala deleted file mode 100644 index 2b1ff1a8..00000000 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala +++ /dev/null @@ -1,88 +0,0 @@ -//package org.terminal21.client.components -// -//import functions.fibers.FiberExecutor -//import org.terminal21.client.* -//import org.terminal21.client.components.UiElement.HasStyle -//import org.terminal21.client.components.chakra.* -//import org.terminal21.collections.TypedMap -// -//import java.util.concurrent.atomic.{AtomicBoolean, AtomicReference} -// -///** Creates a standard UI for a calculation which may take time. While the calculation runs, the UI is grayed out, including the dataUi component. When the -// * calculation completes, it allows for updating the dataUi component. -// * @tparam OUT -// * the return value of the calculation. -// */ -//abstract class StdUiCalculation[OUT]( -// val key: String, -// name: String, -// dataUi: UiElement with HasStyle, -// val dataStore: TypedMap = TypedMap.empty -//)(using session: ConnectedSession, executor: FiberExecutor) -// extends Calculation[OUT] -// with UiComponent: -// private val running = new AtomicBoolean(false) -// private val currentUi = new AtomicReference(dataUi) -// -// protected def updateUi(dataUi: UiElement & HasStyle) = currentUi.set(dataUi) -// -// lazy val badge = Badge() -// lazy val recalc = Button(text = "Recalculate", size = Some("sm"), leftIcon = Some(RepeatIcon())) -// // .onClick(model) -// // if running.compareAndSet(false, true) then -// // try -// // reCalculate() -// // finally running.set(false) -// // handled -// -// override lazy val rendered: Seq[UiElement] = -// val header = Box( -// bg = "green", -// p = 4, -// children = Seq( -// HStack(children = Seq(Text(text = name), badge, recalc)) -// ) -// ) -// Seq(header, dataUi) -// -// override def onError(t: Throwable): Unit = -// session.fireEvent( -// RenderChangesEvent( -// Seq( -// badge.withText(s"Error: ${t.getMessage}").withColorScheme(Some("red")), -// dataUi, -// recalc.withIsDisabled(None) -// ) -// ) -// ) -// super.onError(t) -// -// override protected def whenResultsNotReady(): Unit = -// session.fireEvent( -// RenderChangesEvent( -// Seq( -// badge.withText("Calculating").withColorScheme(Some("purple")), -// currentUi.get().withStyle(dataUi.style + ("filter" -> "grayscale(100%)")), -// recalc.withIsDisabled(Some(true)) -// ) -// ) -// ) -// super.whenResultsNotReady() -// -// override type This = StdUiCalculation[OUT] -// -// // probably this class needs redesign -// override def withKey(key: String): StdUiCalculation[OUT] = ??? -// override def withDataStore(ds: TypedMap): StdUiCalculation[OUT] = ??? -// -// override protected def whenResultsReady(results: OUT): Unit = -// val newDataUi = currentUi.get().withStyle(dataUi.style - "filter") -// session.fireEvent( -// RenderChangesEvent( -// Seq( -// badge.withText("Ready").withColorScheme(None), -// newDataUi, -// recalc.withIsDisabled(Some(false)) -// ) -// ) -// ) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala index 78417392..6049a2f7 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala @@ -28,7 +28,7 @@ case class Button( isLoading: Option[Boolean] = None, isAttached: Option[Boolean] = None, spacing: Option[String] = None, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with OnClickEventHandler.CanHandleOnClickEvent: type This = Button @@ -62,7 +62,7 @@ case class ButtonGroup( borderColor: Option[String] = None, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren: type This = ButtonGroup @@ -90,7 +90,7 @@ case class Box( style: Map[String, Any] = Map.empty, as: Option[String] = None, children: Seq[UiElement] = Nil, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren: type This = Box @@ -113,7 +113,7 @@ case class HStack( align: Option[String] = None, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren: type This = HStack @@ -130,7 +130,7 @@ case class VStack( align: Option[String] = None, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren: type This = VStack @@ -149,7 +149,7 @@ case class SimpleGrid( columns: Int = 2, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren: type This = SimpleGrid @@ -169,7 +169,7 @@ case class Editable( defaultValue: String = "", style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren with OnChangeEventHandler.CanHandleOnChangeEvent: @@ -180,19 +180,19 @@ case class Editable( def withDefaultValue(v: String) = copy(defaultValue = v) override def withDataStore(ds: TypedMap): Editable = copy(dataStore = ds) -case class EditablePreview(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) extends ChakraElement: +case class EditablePreview(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty) extends ChakraElement: type This = EditablePreview override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) override def withDataStore(ds: TypedMap) = copy(dataStore = ds) -case class EditableInput(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) extends ChakraElement: +case class EditableInput(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty) extends ChakraElement: type This = EditableInput override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) override def withDataStore(ds: TypedMap) = copy(dataStore = ds) -case class EditableTextarea(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) extends ChakraElement: +case class EditableTextarea(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty) extends ChakraElement: type This = EditableTextarea override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -205,7 +205,7 @@ case class FormControl( as: String = "", style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren: type This = FormControl @@ -222,7 +222,7 @@ case class FormLabel( text: String, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren: type This = FormLabel @@ -239,7 +239,7 @@ case class FormHelperText( text: String, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren: type This = FormHelperText @@ -259,7 +259,7 @@ case class Input( variant: Option[String] = None, defaultValue: String = "", style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with OnChangeEventHandler.CanHandleOnChangeEvent: type This = Input @@ -277,7 +277,7 @@ case class InputGroup( size: String = "md", style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren: type This = InputGroup @@ -292,7 +292,7 @@ case class InputLeftAddon( text: String = "", style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren: type This = InputLeftAddon @@ -307,7 +307,7 @@ case class InputRightAddon( text: String = "", style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren: type This = InputRightAddon @@ -325,7 +325,7 @@ case class Checkbox( defaultChecked: Boolean = false, isDisabled: Boolean = false, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with OnChangeBooleanEventHandler.CanHandleOnChangeEvent: type This = Checkbox @@ -344,7 +344,7 @@ case class Radio( text: String = "", colorScheme: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = Radio override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -359,7 +359,7 @@ case class RadioGroup( defaultValue: String = "", style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren with OnChangeEventHandler.CanHandleOnChangeEvent: @@ -379,7 +379,7 @@ case class Center( h: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren: type This = Center @@ -402,7 +402,7 @@ case class Circle( h: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren: type This = Circle @@ -425,7 +425,7 @@ case class Square( h: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren: type This = Square @@ -448,7 +448,7 @@ case class AddIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = AddIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -468,7 +468,7 @@ case class ArrowBackIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = ArrowBackIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -488,7 +488,7 @@ case class ArrowDownIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = ArrowDownIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -508,7 +508,7 @@ case class ArrowForwardIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = ArrowForwardIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -528,7 +528,7 @@ case class ArrowLeftIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = ArrowLeftIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -548,7 +548,7 @@ case class ArrowRightIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = ArrowRightIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -568,7 +568,7 @@ case class ArrowUpIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = ArrowUpIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -588,7 +588,7 @@ case class ArrowUpDownIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = ArrowUpDownIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -608,7 +608,7 @@ case class AtSignIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = AtSignIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -628,7 +628,7 @@ case class AttachmentIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = AttachmentIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -648,7 +648,7 @@ case class BellIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = BellIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -668,7 +668,7 @@ case class CalendarIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = CalendarIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -688,7 +688,7 @@ case class ChatIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = ChatIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -708,7 +708,7 @@ case class CheckIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = CheckIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -728,7 +728,7 @@ case class CheckCircleIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = CheckCircleIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -748,7 +748,7 @@ case class ChevronDownIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = ChevronDownIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -768,7 +768,7 @@ case class ChevronLeftIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = ChevronLeftIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -788,7 +788,7 @@ case class ChevronRightIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = ChevronRightIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -808,7 +808,7 @@ case class ChevronUpIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = ChevronUpIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -828,7 +828,7 @@ case class CloseIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = CloseIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -848,7 +848,7 @@ case class CopyIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = CopyIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -868,7 +868,7 @@ case class DeleteIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = DeleteIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -888,7 +888,7 @@ case class DownloadIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = DownloadIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -908,7 +908,7 @@ case class DragHandleIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = DragHandleIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -928,7 +928,7 @@ case class EditIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = EditIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -948,7 +948,7 @@ case class EmailIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = EmailIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -969,7 +969,7 @@ case class ExternalLinkIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = ExternalLinkIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -990,7 +990,7 @@ case class HamburgerIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = HamburgerIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1010,7 +1010,7 @@ case class InfoIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = InfoIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1030,7 +1030,7 @@ case class InfoOutlineIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = InfoOutlineIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1050,7 +1050,7 @@ case class LinkIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = LinkIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1070,7 +1070,7 @@ case class LockIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = LockIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1090,7 +1090,7 @@ case class MinusIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = MinusIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1110,7 +1110,7 @@ case class MoonIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = MoonIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1130,7 +1130,7 @@ case class NotAllowedIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = NotAllowedIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1150,7 +1150,7 @@ case class PhoneIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = PhoneIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1170,7 +1170,7 @@ case class PlusSquareIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = PlusSquareIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1190,7 +1190,7 @@ case class QuestionIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = QuestionIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1210,7 +1210,7 @@ case class QuestionOutlineIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = QuestionOutlineIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1230,7 +1230,7 @@ case class RepeatIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = RepeatIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1250,7 +1250,7 @@ case class RepeatClockIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = RepeatClockIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1270,7 +1270,7 @@ case class SearchIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = SearchIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1290,7 +1290,7 @@ case class Search2Icon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = Search2Icon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1310,7 +1310,7 @@ case class SettingsIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = SettingsIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1330,7 +1330,7 @@ case class SmallAddIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = SmallAddIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1350,7 +1350,7 @@ case class SmallCloseIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = SmallCloseIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1370,7 +1370,7 @@ case class SpinnerIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = SpinnerIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1390,7 +1390,7 @@ case class StarIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = StarIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1410,7 +1410,7 @@ case class SunIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = SunIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1430,7 +1430,7 @@ case class TimeIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = TimeIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1450,7 +1450,7 @@ case class TriangleDownIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = TriangleDownIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1470,7 +1470,7 @@ case class TriangleUpIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = TriangleUpIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1490,7 +1490,7 @@ case class UnlockIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = UnlockIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1510,7 +1510,7 @@ case class UpDownIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = UpDownIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1530,7 +1530,7 @@ case class ViewIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = ViewIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1550,7 +1550,7 @@ case class ViewOffIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = ViewOffIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1570,7 +1570,7 @@ case class WarningIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = WarningIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1590,7 +1590,7 @@ case class WarningTwoIcon( boxSize: Option[String] = None, color: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = WarningTwoIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1611,7 +1611,7 @@ case class Textarea( variant: Option[String] = None, defaultValue: String = "", style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with OnChangeEventHandler.CanHandleOnChangeEvent: type This = Textarea @@ -1632,7 +1632,7 @@ case class Switch( defaultChecked: Boolean = false, isDisabled: Boolean = false, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with OnChangeBooleanEventHandler.CanHandleOnChangeEvent: type This = Switch @@ -1654,7 +1654,7 @@ case class Select( borderColor: Option[String] = None, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren with OnChangeEventHandler.CanHandleOnChangeEvent: @@ -1674,7 +1674,7 @@ case class Option_( value: String, text: String = "", style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = Option_ override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1685,7 +1685,7 @@ case class Option_( /** https://chakra-ui.com/docs/components/table/usage */ -case class TableContainer(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) +case class TableContainer(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty) extends ChakraElement with HasChildren: type This = TableContainer @@ -1717,7 +1717,7 @@ case class Table( colorScheme: Option[String] = None, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren: type This = Table @@ -1729,7 +1729,7 @@ case class Table( def withColorScheme(v: Option[String]) = copy(colorScheme = v) override def withDataStore(ds: TypedMap) = copy(dataStore = ds) -case class TableCaption(key: String = Keys.nextKey, text: String = "", style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) +case class TableCaption(key: String = Keys.nextKey, text: String = "", style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty) extends ChakraElement: type This = TableCaption override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1737,7 +1737,7 @@ case class TableCaption(key: String = Keys.nextKey, text: String = "", style: Ma def withText(v: String) = copy(text = v) override def withDataStore(ds: TypedMap) = copy(dataStore = ds) -case class Thead(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) +case class Thead(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty) extends ChakraElement with HasChildren: type This = Thead @@ -1746,7 +1746,7 @@ case class Thead(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, sty def withKey(v: String) = copy(key = v) override def withDataStore(ds: TypedMap) = copy(dataStore = ds) -case class Tbody(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) +case class Tbody(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty) extends ChakraElement with HasChildren: type This = Tbody @@ -1755,7 +1755,7 @@ case class Tbody(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, sty def withKey(v: String) = copy(key = v) override def withDataStore(ds: TypedMap) = copy(dataStore = ds) -case class Tfoot(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) +case class Tfoot(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty) extends ChakraElement with HasChildren: type This = Tfoot @@ -1768,7 +1768,7 @@ case class Tr( key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren: type This = Tr @@ -1783,7 +1783,7 @@ case class Th( isNumeric: Boolean = false, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren: type This = Th @@ -1800,7 +1800,7 @@ case class Td( isNumeric: Boolean = false, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren: type This = Td @@ -1813,7 +1813,7 @@ case class Td( /** https://chakra-ui.com/docs/components/menu/usage */ -case class Menu(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, dataStore: TypedMap = TypedMap.empty) +case class Menu(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, dataStore: TypedMap = TypedMap.Empty) extends ChakraElement with HasChildren: type This = Menu @@ -1829,7 +1829,7 @@ case class MenuButton( colorScheme: Option[String] = None, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren: type This = MenuButton @@ -1841,7 +1841,7 @@ case class MenuButton( def withColorScheme(v: Option[String]) = copy(colorScheme = v) override def withDataStore(ds: TypedMap) = copy(dataStore = ds) -case class MenuList(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, dataStore: TypedMap = TypedMap.empty) +case class MenuList(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, dataStore: TypedMap = TypedMap.Empty) extends ChakraElement with HasChildren: type This = MenuList @@ -1855,7 +1855,7 @@ case class MenuItem( style: Map[String, Any] = Map.empty, text: String = "", children: Seq[UiElement] = Nil, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren with OnClickEventHandler.CanHandleOnClickEvent: @@ -1866,7 +1866,7 @@ case class MenuItem( def withText(v: String) = copy(text = v) override def withDataStore(ds: TypedMap): MenuItem = copy(dataStore = ds) -case class MenuDivider(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) extends ChakraElement: +case class MenuDivider(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty) extends ChakraElement: type This = MenuDivider override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -1880,7 +1880,7 @@ case class Badge( size: String = "md", children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren: type This = Badge @@ -1906,7 +1906,7 @@ case class Image( boxSize: Option[String] = None, borderRadius: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = Image override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1930,7 +1930,7 @@ case class Text( casing: Option[String] = None, decoration: Option[String] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = Text override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -1951,7 +1951,7 @@ case class Code( colorScheme: Option[String] = None, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren: type This = Code @@ -1967,7 +1967,7 @@ case class UnorderedList( spacing: Option[String] = None, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren: type This = UnorderedList @@ -1982,7 +1982,7 @@ case class OrderedList( spacing: Option[String] = None, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren: type This = OrderedList @@ -1997,7 +1997,7 @@ case class ListItem( text: String = "", style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren: type This = ListItem @@ -2012,7 +2012,7 @@ case class Alert( status: String = "error", // error, success, warning, info style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren: type This = Alert @@ -2025,7 +2025,7 @@ case class Alert( case class AlertIcon( key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = AlertIcon override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -2036,7 +2036,7 @@ case class AlertTitle( key: String = Keys.nextKey, text: String = "Alert!", style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = AlertTitle override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -2048,7 +2048,7 @@ case class AlertDescription( key: String = Keys.nextKey, text: String = "Something happened!", style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = AlertDescription override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -2066,7 +2066,7 @@ case class Progress( hasStripe: Option[Boolean] = None, isIndeterminate: Option[Boolean] = None, style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement: type This = Progress override def withStyle(v: Map[String, Any]) = copy(style = v) @@ -2087,7 +2087,7 @@ case class Tooltip( fontSize: Option[String] = None, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Seq(Text("use tooltip.withContent() to set this")), - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren: type This = Tooltip @@ -2115,7 +2115,7 @@ case class Tabs( isFitted: Option[Boolean] = None, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren: type This = Tabs @@ -2135,7 +2135,7 @@ case class TabList( key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren: type This = TabList @@ -2155,7 +2155,7 @@ case class Tab( _active: Option[Map[String, Any]] = None, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren: type This = Tab @@ -2178,7 +2178,7 @@ case class TabPanels( key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren: type This = TabPanels @@ -2193,7 +2193,7 @@ case class TabPanel( key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren: type This = TabPanel @@ -2213,7 +2213,7 @@ case class Breadcrumb( pt: Option[Int] = None, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren: type This = Breadcrumb @@ -2234,7 +2234,7 @@ case class BreadcrumbItem( isCurrentPage: Option[Boolean] = None, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren: type This = BreadcrumbItem @@ -2252,7 +2252,7 @@ case class BreadcrumbLink( href: Option[String] = None, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren with OnClickEventHandler.CanHandleOnClickEvent: @@ -2272,7 +2272,7 @@ case class Link( color: Option[String] = None, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement with HasChildren with OnClickEventHandler.CanHandleOnClickEvent: diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala index f5bb399c..2bb9508e 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala @@ -10,7 +10,7 @@ case class QuickFormControl( label: Option[String] = None, inputGroup: Seq[UiElement] = Nil, helperText: Option[String] = None, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends UiComponent with HasStyle: type This = QuickFormControl diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala index 2f73c735..04072176 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala @@ -13,7 +13,7 @@ case class QuickTable( caption: Option[String] = None, headers: Seq[Any] = Nil, rows: Seq[Seq[Any]] = Nil, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends UiComponent with HasStyle: type This = QuickTable diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala index 5b5cdda6..fe5816f1 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala @@ -9,7 +9,7 @@ case class QuickTabs( style: Map[String, Any] = Map.empty, tabs: Seq[String | Seq[UiElement]] = Nil, tabPanels: Seq[Seq[UiElement]] = Nil, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends UiComponent with HasStyle: type This = QuickTabs diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/frontend/FrontEndElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/frontend/FrontEndElement.scala index b4ff803b..f4422ebf 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/frontend/FrontEndElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/frontend/FrontEndElement.scala @@ -5,7 +5,7 @@ import org.terminal21.collections.TypedMap sealed trait FrontEndElement extends UiElement -case class ThemeToggle(key: String = Keys.nextKey, dataStore: TypedMap = TypedMap.empty) extends FrontEndElement: +case class ThemeToggle(key: String = Keys.nextKey, dataStore: TypedMap = TypedMap.Empty) extends FrontEndElement: override type This = ThemeToggle override def withKey(key: String): ThemeToggle = copy(key = key) override def withDataStore(ds: TypedMap) = copy(dataStore = ds) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala index d4e357d3..1bfb5d3e 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala @@ -8,62 +8,62 @@ import org.terminal21.collections.TypedMap sealed trait StdEJson extends UiElement sealed trait StdElement extends StdEJson with HasStyle -case class Span(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) extends StdElement: +case class Span(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty) extends StdElement: type This = Span override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) override def withDataStore(ds: TypedMap) = copy(dataStore = ds) -case class NewLine(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) extends StdElement: +case class NewLine(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty) extends StdElement: type This = NewLine override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) override def withDataStore(ds: TypedMap) = copy(dataStore = ds) -case class Em(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) extends StdElement: +case class Em(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty) extends StdElement: type This = Em override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) override def withDataStore(ds: TypedMap) = copy(dataStore = ds) -case class Header1(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) extends StdElement: +case class Header1(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty) extends StdElement: type This = Header1 override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) override def withDataStore(ds: TypedMap) = copy(dataStore = ds) -case class Header2(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) extends StdElement: +case class Header2(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty) extends StdElement: type This = Header2 override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) override def withDataStore(ds: TypedMap) = copy(dataStore = ds) -case class Header3(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) extends StdElement: +case class Header3(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty) extends StdElement: type This = Header3 override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) override def withDataStore(ds: TypedMap) = copy(dataStore = ds) -case class Header4(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) extends StdElement: +case class Header4(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty) extends StdElement: type This = Header4 override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) override def withDataStore(ds: TypedMap) = copy(dataStore = ds) -case class Header5(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) extends StdElement: +case class Header5(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty) extends StdElement: type This = Header5 override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withText(v: String) = copy(text = v) override def withDataStore(ds: TypedMap) = copy(dataStore = ds) -case class Header6(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.empty) extends StdElement: +case class Header6(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty) extends StdElement: type This = Header6 override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) @@ -75,7 +75,7 @@ case class Paragraph( text: String = "", style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends StdElement with HasChildren: type This = Paragraph @@ -90,7 +90,7 @@ case class Input( `type`: String = "text", defaultValue: String = "", style: Map[String, Any] = Map.empty, - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends StdElement with CanHandleOnChangeEvent: type This = Input diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala index ed13b590..18ed771f 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala @@ -28,7 +28,7 @@ case class Cookie( path: Option[String] = None, expireDays: Option[Int] = None, requestId: String = "cookie-set-req", - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends StdHttp: override type This = Cookie override def withKey(key: String): Cookie = copy(key = key) @@ -41,7 +41,7 @@ case class CookieReader( key: String = Keys.nextKey, name: String = "cookie.name", requestId: String = "cookie-read-req", - dataStore: TypedMap = TypedMap.empty + dataStore: TypedMap = TypedMap.Empty ) extends StdHttp with CanHandleOnChangeEvent: type This = CookieReader diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/CalculationTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/CalculationTest.scala deleted file mode 100644 index 9fea22ac..00000000 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/CalculationTest.scala +++ /dev/null @@ -1,37 +0,0 @@ -package org.terminal21.client - -import functions.fibers.FiberExecutor -import org.scalatest.funsuite.AnyFunSuiteLike -import org.scalatest.matchers.should.Matchers.* - -import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger} -import org.scalatest.concurrent.Eventually.* -import org.terminal21.client.components.Calculation - -class CalculationTest extends AnyFunSuiteLike: - given executor: FiberExecutor = FiberExecutor() - def testCalc(i: Int) = i + 1 - def testCalcString(i: Int): String = (i + 10).toString - - class Calc extends Calculation[Int]: - val whenResultsNotReadyCalled = new AtomicBoolean(false) - val whenResultsReadyValue = new AtomicInteger(-1) - override protected def whenResultsNotReady(): Unit = whenResultsNotReadyCalled.set(true) - override protected def whenResultsReady(results: Int): Unit = whenResultsReadyValue.set(results) - override protected def calculation() = 2 - - test("calculates"): - val calc = new Calc - calc.run().get() should be(2) - - test("calls whenResultsNotReady"): - val calc = new Calc - calc.run() - eventually: - calc.whenResultsNotReadyCalled.get() should be(true) - - test("calls whenResultsReady"): - val calc = new Calc - calc.run() - eventually: - calc.whenResultsReadyValue.get() should be(2) From 9bc93b53b8d043ca29eaf274f177b92c20afd99c Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 6 Mar 2024 15:09:46 +0000 Subject: [PATCH 290/313] - --- .../org/terminal21/sparklib/calculations/SparkCalculation.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala index be2f8e13..4de2df8b 100644 --- a/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala +++ b/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala @@ -80,7 +80,7 @@ case class SparkCalculation[OUT: ReadWriter]( HStack(children = Seq(Text(text = name), badge, recalc)) ) ) - val ui = out.map(toUi).getOrElse(dataUi) + val ui = dataUi println(ui) Seq(header, ui) From 9a8fa073d78d264a48474760a6fd6b7a7094a0e7 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 6 Mar 2024 16:30:25 +0000 Subject: [PATCH 291/313] - --- .../src/main/scala/tests/RunAll.scala | 2 +- .../org/terminal21/sparklib/Cached.scala | 58 +++++++++++++++++++ .../sparklib/CalculationsExtensions.scala | 21 ------- .../calculations/SparkCalculation.scala | 47 ++++----------- .../sparklib/endtoend/SparkBasics.scala | 36 +++++++----- .../scala/org/terminal21/client/Globals.scala | 3 +- 6 files changed, 94 insertions(+), 73 deletions(-) create mode 100644 terminal21-spark/src/main/scala/org/terminal21/sparklib/Cached.scala delete mode 100644 terminal21-spark/src/main/scala/org/terminal21/sparklib/CalculationsExtensions.scala diff --git a/end-to-end-tests/src/main/scala/tests/RunAll.scala b/end-to-end-tests/src/main/scala/tests/RunAll.scala index 14e48976..fa0eabba 100644 --- a/end-to-end-tests/src/main/scala/tests/RunAll.scala +++ b/end-to-end-tests/src/main/scala/tests/RunAll.scala @@ -1,7 +1,7 @@ package tests import functions.fibers.Fiber -import org.terminal21.client.given +import org.terminal21.client.* @main def runAll(): Unit = Seq( diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/Cached.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/Cached.scala new file mode 100644 index 00000000..4b842737 --- /dev/null +++ b/terminal21-spark/src/main/scala/org/terminal21/sparklib/Cached.scala @@ -0,0 +1,58 @@ +package org.terminal21.sparklib + +import functions.fibers.FiberExecutor +import org.apache.commons.io.FileUtils +import org.apache.spark.sql.SparkSession +import org.terminal21.client.* +import org.terminal21.client.components.UiElement +import org.terminal21.client.components.UiElement.HasStyle +import org.terminal21.sparklib.calculations.SparkCalculation.TriggerRedraw +import org.terminal21.sparklib.calculations.{ReadWriter, SparkCalculation} +import org.terminal21.sparklib.util.Environment + +import java.io.File + +class Cached[OUT: ReadWriter](val name: String, outF: => OUT)(using spark: SparkSession): + private val rw = implicitly[ReadWriter[OUT]] + private val rootFolder = s"${Environment.tmpDirectory}/spark-calculations" + private val targetDir = s"$rootFolder/$name" + + def isCached: Boolean = new File(targetDir).exists() + + def cachePath: String = targetDir + + private def cache[A](reader: => A, writer: => A): A = + if isCached then reader + else writer + + def invalidateCache(): Unit = + FileUtils.deleteDirectory(new File(targetDir)) + + private def calculateOnce: OUT = + cache( + rw.read(spark, targetDir), { + val ds = outF + rw.write(targetDir, ds) + ds + } + ) + + @volatile private var out = Option.empty[OUT] + private def startCalc(session: ConnectedSession): Unit = + fiberExecutor.submit: + out = Some(calculateOnce) + session.fireEvent(TriggerRedraw) + + def get: Option[OUT] = out + + def visualize(dataUi: UiElement & HasStyle)( + toUi: OUT => UiElement & HasStyle + )(using + FiberExecutor, + SparkSession + )(using session: ConnectedSession, events: Events) = + if events.event != TriggerRedraw then startCalc(session) + new SparkCalculation[OUT](s"spark-calc-$name", dataUi, toUi, this) + +object Cached: + def apply[OUT: ReadWriter](name: String)(outF: => OUT)(using spark: SparkSession): Cached[OUT] = new Cached(name, outF) diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/CalculationsExtensions.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/CalculationsExtensions.scala deleted file mode 100644 index 8cfa21b7..00000000 --- a/terminal21-spark/src/main/scala/org/terminal21/sparklib/CalculationsExtensions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package org.terminal21.sparklib - -import functions.fibers.FiberExecutor -import org.apache.spark.sql.SparkSession -import org.terminal21.client.* -import org.terminal21.client.components.UiElement -import org.terminal21.client.components.UiElement.HasStyle -import org.terminal21.sparklib.calculations.{ReadWriter, SparkCalculation} - -extension [OUT: ReadWriter](ds: OUT) - def visualize(name: String, dataUi: UiElement & HasStyle)( - toUi: OUT => UiElement & HasStyle - )(using - ConnectedSession, - FiberExecutor, - SparkSession, - Events - ) = - val ui = new SparkCalculation[OUT](s"spark-calc-$name", name, dataUi, toUi, ds) - ui.runCalculation() - ui diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala index 4de2df8b..c287d325 100644 --- a/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala +++ b/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala @@ -9,6 +9,7 @@ import org.terminal21.client.components.chakra.* import org.terminal21.client.components.* import org.terminal21.collections.TypedMap import org.terminal21.model.ClientEvent +import org.terminal21.sparklib.Cached import org.terminal21.sparklib.calculations.SparkCalculation.TriggerRedraw import org.terminal21.sparklib.util.Environment @@ -23,53 +24,22 @@ import java.io.File */ case class SparkCalculation[OUT: ReadWriter]( key: String, - name: String, dataUi: UiElement with HasStyle, toUi: OUT => UiElement & HasStyle, - dataSet: OUT, + cached: Cached[OUT], dataStore: TypedMap = TypedMap.Empty )(using spark: SparkSession, session: ConnectedSession, events: Events ) extends UiComponent: + def name = cached.name override type This = SparkCalculation[OUT] override def withKey(key: String): This = copy(key = key) override def withDataStore(ds: TypedMap): This = copy(dataStore = ds) - private val rw = implicitly[ReadWriter[OUT]] - private val rootFolder = s"${Environment.tmpDirectory}/spark-calculations" - private val targetDir = s"$rootFolder/$name" - - def isCached: Boolean = new File(targetDir).exists() - def cachePath: String = targetDir - - private def cache[A](reader: => A, writer: => A): A = - if isCached then reader - else writer - - def invalidateCache(): Unit = - FileUtils.deleteDirectory(new File(targetDir)) - - private def calculateOnce(f: => OUT): OUT = - cache( - rw.read(spark, targetDir), { - val ds = f - rw.write(targetDir, ds) - ds - } - ) - - def runCalculation(): Unit = - if events.event != TriggerRedraw then - fiberExecutor.submit: - println("runCalculation()") - val r = calculateOnce(dataSet) - println("Redraw") - session.fireEvent(TriggerRedraw) - r - val badge = Badge(s"recalc-badge-$name") - val recalc = Button(s"recalc-button-$name", text = "Recalculate", size = Some("sm"), leftIcon = Some(RepeatIcon())) + val badge = Badge(s"recalc-badge-$name") + val recalc = Button(s"recalc-button-$name", text = "Recalculate", size = Some("sm"), leftIcon = Some(RepeatIcon())) override def rendered: Seq[UiElement] = val header = Box( @@ -80,8 +50,11 @@ case class SparkCalculation[OUT: ReadWriter]( HStack(children = Seq(Text(text = name), badge, recalc)) ) ) - val ui = dataUi - println(ui) + val ui = cached.get + .map: ds => + toUi(ds) + .getOrElse(dataUi) + Seq(header, ui) object SparkCalculation: diff --git a/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala b/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala index 54ea3402..fccf54d9 100644 --- a/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala +++ b/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala @@ -25,27 +25,38 @@ import scala.util.Using val headers = Seq("id", "name", "path", "numOfLines", "numOfWords", "createdDate", "timestamp") + val sortedSourceFilesDS = Cached("Sorted files"): + sortedSourceFiles(sourceFiles()).limit(3) + val sourceFileCached = Cached("Code files"): + sourceFiles().limit(3) + val sortedSourceFilesDFCached = Cached("Sorted files DF"): + sourceFiles() + .sort($"createdDate".asc, $"numOfWords".asc) + .toDF() + .limit(4) + + val sourceFilesSortedByNumOfLinesCached = Cached("Biggest Code Files"): + sourceFiles() + .sort($"numOfLines".desc) + def components(events: Events) = println("components() START") given Events = events val sortedFilesTable = QuickTable().withHeaders(headers: _*).caption("Files sorted by createdDate and numOfWords") val codeFilesTable = QuickTable().withHeaders(headers: _*).caption("Unsorted files") - val sortedSourceFilesDS = sortedSourceFiles(sourceFiles()) - val sortedCalc = sortedSourceFilesDS.visualize("Sorted files", sortedFilesTable): results => - val tableRows = results.take(3).toList.map(_.toData) + val sortedCalc = sortedSourceFilesDS.visualize(sortedFilesTable): results => + val tableRows = results.collect().map(_.toData).toList sortedFilesTable.withRows(tableRows) - val codeFilesCalculation = sourceFiles().visualize("Code files", codeFilesTable): results => - val dt = results.take(3).toList + val codeFilesCalculation = sourceFileCached.visualize(codeFilesTable): results => + val dt = results.collect().toList codeFilesTable.withRows(dt.map(_.toData)) val sortedFilesTableDF = QuickTable().withHeaders(headers: _*).caption("Files sorted by createdDate and numOfWords ASC and as DF") - val sortedCalcAsDF = sourceFiles() - .sort($"createdDate".asc, $"numOfWords".asc) - .toDF() - .visualize("Sorted files DF", sortedFilesTableDF): results => - val tableRows = results.take(4).toList + val sortedCalcAsDF = sortedSourceFilesDFCached + .visualize(sortedFilesTableDF): results => + val tableRows = results.collect().toList sortedFilesTableDF.withRows(tableRows.toUiTable) val chart = ResponsiveLine( @@ -60,9 +71,8 @@ import scala.util.Using legends = Seq(Legend()) ) - val sourceFileChart = sourceFiles() - .sort($"numOfLines".desc) - .visualize("Biggest Code Files", chart): results => + val sourceFileChart = sourceFilesSortedByNumOfLinesCached + .visualize(chart): results => val data = results.take(10).map(cf => Datum(StringUtils.substringBeforeLast(cf.name, ".scala"), cf.numOfLines)).toList chart.withData(Seq(Serie("Scala", data = data))) println("components() RETURNING") diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Globals.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Globals.scala index 74a3af9b..b1f69c85 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Globals.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Globals.scala @@ -2,4 +2,5 @@ package org.terminal21.client import functions.fibers.FiberExecutor -given fiberExecutor: FiberExecutor = FiberExecutor() +given FiberExecutor = FiberExecutor() +val fiberExecutor = implicitly[FiberExecutor] From 6abe4ceef7435c236cf244c6a4f33a6ebcf601c2 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 6 Mar 2024 16:46:50 +0000 Subject: [PATCH 292/313] - --- build.sbt | 6 +++--- .../main/scala/org/terminal21/sparklib/Cached.scala | 11 +++++++++-- .../terminal21/sparklib/endtoend/SparkBasics.scala | 11 ++++++----- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/build.sbt b/build.sbt index 9c8ba28f..a51dd017 100644 --- a/build.sbt +++ b/build.sbt @@ -50,10 +50,10 @@ val HelidonServerLogging = "io.helidon.logging" % "helidon-logging-jul" val LogBack = "ch.qos.logback" % "logback-classic" % "1.4.14" val Slf4jApi = "org.slf4j" % "slf4j-api" % "2.0.9" -val SparkSql = ("org.apache.spark" %% "spark-sql" % "3.5.0" % "provided").cross(CrossVersion.for3Use2_13).exclude("org.scala-lang.modules", "scala-xml_2.13") +val SparkSql = ("org.apache.spark" %% "spark-sql" % "3.5.1" % "provided").cross(CrossVersion.for3Use2_13).exclude("org.scala-lang.modules", "scala-xml_2.13") val SparkScala3Fix = Seq( - "io.github.vincenzobaz" %% "spark-scala3-encoders" % "0.2.5", - "io.github.vincenzobaz" %% "spark-scala3-udf" % "0.2.5" + "io.github.vincenzobaz" %% "spark-scala3-encoders" % "0.2.6", + "io.github.vincenzobaz" %% "spark-scala3-udf" % "0.2.6" ).map(_.exclude("org.scala-lang.modules", "scala-xml_2.13")) // ----------------------------------------------------------------------------------------------- diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/Cached.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/Cached.scala index 4b842737..405e9a24 100644 --- a/terminal21-spark/src/main/scala/org/terminal21/sparklib/Cached.scala +++ b/terminal21-spark/src/main/scala/org/terminal21/sparklib/Cached.scala @@ -27,6 +27,7 @@ class Cached[OUT: ReadWriter](val name: String, outF: => OUT)(using spark: Spark def invalidateCache(): Unit = FileUtils.deleteDirectory(new File(targetDir)) + out = None private def calculateOnce: OUT = cache( @@ -51,8 +52,14 @@ class Cached[OUT: ReadWriter](val name: String, outF: => OUT)(using spark: Spark FiberExecutor, SparkSession )(using session: ConnectedSession, events: Events) = - if events.event != TriggerRedraw then startCalc(session) - new SparkCalculation[OUT](s"spark-calc-$name", dataUi, toUi, this) + val sc = new SparkCalculation[OUT](s"spark-calc-$name", dataUi, toUi, this) + + if events.isClicked(sc.recalc) then + invalidateCache() + startCalc(session) + else if events.event != TriggerRedraw then startCalc(session) + + sc object Cached: def apply[OUT: ReadWriter](name: String)(outF: => OUT)(using spark: SparkSession): Cached[OUT] = new Cached(name, outF) diff --git a/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala b/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala index fccf54d9..b219d4bf 100644 --- a/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala +++ b/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala @@ -23,12 +23,10 @@ import scala.util.Using import scala3encoders.given import spark.implicits.* - val headers = Seq("id", "name", "path", "numOfLines", "numOfWords", "createdDate", "timestamp") - - val sortedSourceFilesDS = Cached("Sorted files"): - sortedSourceFiles(sourceFiles()).limit(3) val sourceFileCached = Cached("Code files"): sourceFiles().limit(3) + val sortedSourceFilesDS = Cached("Sorted files"): + sortedSourceFiles(sourceFiles()).limit(3) val sortedSourceFilesDFCached = Cached("Sorted files DF"): sourceFiles() .sort($"createdDate".asc, $"numOfWords".asc) @@ -39,9 +37,12 @@ import scala.util.Using sourceFiles() .sort($"numOfLines".desc) + println(s"Cached dir: ${sourceFileCached.cachePath}") def components(events: Events) = println("components() START") - given Events = events + given Events = events + + val headers = Seq("id", "name", "path", "numOfLines", "numOfWords", "createdDate", "timestamp") val sortedFilesTable = QuickTable().withHeaders(headers: _*).caption("Files sorted by createdDate and numOfWords") val codeFilesTable = QuickTable().withHeaders(headers: _*).caption("Unsorted files") From 9456edb093803222037a6413b8824f153bfc7999 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 6 Mar 2024 16:57:11 +0000 Subject: [PATCH 293/313] - --- .../src/main/scala/org/terminal21/sparklib/Cached.scala | 5 +++-- .../sparklib/calculations/SparkCalculation.scala | 8 ++++++-- .../org/terminal21/sparklib/endtoend/SparkBasics.scala | 2 -- .../src/main/scala/org/terminal21/client/Controller.scala | 3 +++ 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/Cached.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/Cached.scala index 405e9a24..5be8403d 100644 --- a/terminal21-spark/src/main/scala/org/terminal21/sparklib/Cached.scala +++ b/terminal21-spark/src/main/scala/org/terminal21/sparklib/Cached.scala @@ -4,6 +4,7 @@ import functions.fibers.FiberExecutor import org.apache.commons.io.FileUtils import org.apache.spark.sql.SparkSession import org.terminal21.client.* +import org.terminal21.client.Events.InitialRender import org.terminal21.client.components.UiElement import org.terminal21.client.components.UiElement.HasStyle import org.terminal21.sparklib.calculations.SparkCalculation.TriggerRedraw @@ -40,6 +41,7 @@ class Cached[OUT: ReadWriter](val name: String, outF: => OUT)(using spark: Spark @volatile private var out = Option.empty[OUT] private def startCalc(session: ConnectedSession): Unit = + println("startCalc()") fiberExecutor.submit: out = Some(calculateOnce) session.fireEvent(TriggerRedraw) @@ -57,8 +59,7 @@ class Cached[OUT: ReadWriter](val name: String, outF: => OUT)(using spark: Spark if events.isClicked(sc.recalc) then invalidateCache() startCalc(session) - else if events.event != TriggerRedraw then startCalc(session) - + else if events.isInitialRender then startCalc(session) sc object Cached: diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala index c287d325..e0af5171 100644 --- a/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala +++ b/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala @@ -38,7 +38,6 @@ case class SparkCalculation[OUT: ReadWriter]( override def withKey(key: String): This = copy(key = key) override def withDataStore(ds: TypedMap): This = copy(dataStore = ds) - val badge = Badge(s"recalc-badge-$name") val recalc = Button(s"recalc-button-$name", text = "Recalculate", size = Some("sm"), leftIcon = Some(RepeatIcon())) override def rendered: Seq[UiElement] = @@ -47,7 +46,12 @@ case class SparkCalculation[OUT: ReadWriter]( bg = "green", p = 4, children = Seq( - HStack(children = Seq(Text(text = name), badge, recalc)) + HStack().withChildren( + Text(text = name), + if events.isClicked(recalc) then Badge(text = "Recalculating...") + else if events.isInitialRender then Badge(text = "Initializing...") + else recalc + ) ) ) val ui = cached.get diff --git a/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala b/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala index b219d4bf..17911df0 100644 --- a/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala +++ b/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala @@ -39,7 +39,6 @@ import scala.util.Using println(s"Cached dir: ${sourceFileCached.cachePath}") def components(events: Events) = - println("components() START") given Events = events val headers = Seq("id", "name", "path", "numOfLines", "numOfWords", "createdDate", "timestamp") @@ -76,7 +75,6 @@ import scala.util.Using .visualize(chart): results => val data = results.take(10).map(cf => Datum(StringUtils.substringBeforeLast(cf.name, ".scala"), cf.numOfLines)).toList chart.withData(Seq(Serie("Scala", data = data))) - println("components() RETURNING") Seq( codeFilesCalculation, sortedCalc, diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index c4a83ed4..3eaa00b1 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -1,5 +1,6 @@ package org.terminal21.client +import org.terminal21.client.Events.InitialRender import org.terminal21.client.collections.EventIterator import org.terminal21.client.components.{OnChangeBooleanEventHandler, OnChangeEventHandler, UiElement} import org.terminal21.client.components.OnClickEventHandler.CanHandleOnClickEvent @@ -80,6 +81,8 @@ case class Events(event: CommandEvent): case OnChange(key, _) => key == e.key case _ => false + def isInitialRender: Boolean = event == InitialRender + object Events: case object InitialRender extends ClientEvent From 628436cf5ae9fce9b5a235d8389f02d605bcfa54 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 6 Mar 2024 17:13:38 +0000 Subject: [PATCH 294/313] - --- .../src/main/scala/org/terminal21/sparklib/Cached.scala | 2 -- 1 file changed, 2 deletions(-) diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/Cached.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/Cached.scala index 5be8403d..3d5d1bd1 100644 --- a/terminal21-spark/src/main/scala/org/terminal21/sparklib/Cached.scala +++ b/terminal21-spark/src/main/scala/org/terminal21/sparklib/Cached.scala @@ -4,7 +4,6 @@ import functions.fibers.FiberExecutor import org.apache.commons.io.FileUtils import org.apache.spark.sql.SparkSession import org.terminal21.client.* -import org.terminal21.client.Events.InitialRender import org.terminal21.client.components.UiElement import org.terminal21.client.components.UiElement.HasStyle import org.terminal21.sparklib.calculations.SparkCalculation.TriggerRedraw @@ -41,7 +40,6 @@ class Cached[OUT: ReadWriter](val name: String, outF: => OUT)(using spark: Spark @volatile private var out = Option.empty[OUT] private def startCalc(session: ConnectedSession): Unit = - println("startCalc()") fiberExecutor.submit: out = Some(calculateOnce) session.fireEvent(TriggerRedraw) From 7c4f021bbfdcefbc7e3fc9c211ce98542a595fd0 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 6 Mar 2024 17:28:03 +0000 Subject: [PATCH 295/313] - --- example-scripts/bouncing-ball.sc | 2 +- example-spark/spark-notebook.sc | 114 +++++++++--------- .../org/terminal21/sparklib/Cached.scala | 1 - 3 files changed, 60 insertions(+), 57 deletions(-) diff --git a/example-scripts/bouncing-ball.sc b/example-scripts/bouncing-ball.sc index 372f6f43..ab9ae53f 100755 --- a/example-scripts/bouncing-ball.sc +++ b/example-scripts/bouncing-ball.sc @@ -6,7 +6,7 @@ // ------------------------------------------------------------------------------ // always import these -import org.terminal21.client.{*, given} +import org.terminal21.client.* import org.terminal21.client.components.* import org.terminal21.model.* // use the chakra components for menus, forms etc, https://chakra-ui.com/docs/components diff --git a/example-spark/spark-notebook.sc b/example-spark/spark-notebook.sc index fc7c0718..54e0ec03 100755 --- a/example-spark/spark-notebook.sc +++ b/example-spark/spark-notebook.sc @@ -7,12 +7,11 @@ * force re-evaluation by clicking the "Recalculate" buttons in the UI. */ -// We need these imports import org.apache.spark.sql.* import org.terminal21.client.components.* import org.terminal21.client.components.chakra.* import org.terminal21.client.components.nivo.* -import org.terminal21.client.{*, given} +import org.terminal21.client.* import org.terminal21.sparklib.* import java.util.concurrent.atomic.AtomicInteger @@ -28,21 +27,19 @@ Using.resource(SparkSessions.newSparkSession( /* configure your spark session he .connect: session => given ConnectedSession = session given SparkSession = spark - given Model[Unit] = Model.Standard.unitModel import scala3encoders.given import spark.implicits.* // lets get a Dataset, the data are random so that when we click refresh we can see the data actually // been refreshed. - val peopleDS = createPeople - - // We will display the data in a table - val peopleTable = QuickTable().withHeaders("Id", "Name", "Age").withCaption("People") - - val peopleTableCalc = peopleDS - .sort($"id") - .visualize("People sample", peopleTable): data => - peopleTable.withRows(data.take(5).map(p => Seq(p.id, p.name, p.age))) + val peopleDS = createPeople + val peopleSample = Cached("People sample"): + peopleDS + .sort($"id") + .limit(5) + val peopleOrderedByAge = Cached("Oldest people"): + peopleDS + .orderBy($"age".desc) /** The calculation above uses a directory to store the dataset results. This way we can restart this script without loosing datasets that may take long * to calculate, making our script behave more like a notebook. When we click "Recalculate" in the UI, the cache directory is deleted and the dataset is @@ -50,54 +47,61 @@ Using.resource(SparkSessions.newSparkSession( /* configure your spark session he * * The key for the cache is "People sample" */ - println(s"Cache path: ${peopleTableCalc.cachePath}") + println(s"Cache path: ${peopleSample.cachePath}") + + def components(events: Events) = + given Events = events + // We will display the data in a table + val peopleTable = QuickTable().withHeaders("Id", "Name", "Age").withCaption("People") + + val peopleTableCalc = peopleSample.visualize(peopleTable): data => + peopleTable.withRows(data.collect.toList.map(p => Seq(p.id, p.name, p.age))) - val oldestPeopleChart = ResponsiveLine( - axisBottom = Some(Axis(legend = "Person", legendOffset = 36)), - axisLeft = Some(Axis(legend = "Age", legendOffset = -40)), - legends = Seq(Legend()) - ) + val oldestPeopleChart = ResponsiveLine( + axisBottom = Some(Axis(legend = "Person", legendOffset = 36)), + axisLeft = Some(Axis(legend = "Age", legendOffset = -40)), + legends = Seq(Legend()) + ) - val oldestPeopleChartCalc = peopleDS - .orderBy($"age".desc) - .visualize("Oldest people", oldestPeopleChart): data => - oldestPeopleChart.withData( - Seq( - Serie( - "Person", - data = data.take(5).map(person => Datum(person.name, person.age)) + val oldestPeopleChartCalc = peopleOrderedByAge + .visualize(oldestPeopleChart): data => + oldestPeopleChart.withData( + Seq( + Serie( + "Person", + data = data.take(5).map(person => Datum(person.name, person.age)) + ) ) ) - ) - val components = Seq( - Paragraph( - text = """ - |The spark notebooks can use the `visualise` extension method over a dataframe/dataset. It will cache the dataset by - |saving it as a file under /tmp. The `Recalculate` button refreshes the dataset (re-runs it). In this example, the - |data are random and so are different each time the `Recalculate` is pressed. - |""".stripMargin, - style = Map("margin" -> "32px") - ), - // just make it look a bit more like a proper notebook by adding some fake maths - MathJax( - expression = """ - |The following is total nonsense but it simulates some explanation that would normally be here if we had - |a proper notebook. When \(a \ne 0\), there are two solutions to \(x = {-b \pm \sqrt{b^2-4ac} \over 2a}.\) - |Aenean vel velit a lacus lacinia pulvinar. Morbi eget ex et tellus aliquam molestie sit amet eu diam. - |Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas tellus enim, tempor non efficitur et, rutrum efficitur metus. - |Nulla scelerisque, mauris sit amet accumsan iaculis, elit ipsum suscipit lorem, sed fermentum nunc purus non tellus. - |Aenean congue accumsan tempor. \(x = {-b \pm \sqrt{b^2-4ac} \over 2a}.\) maecenas vitae commodo tortor. Aliquam erat volutpat. Etiam laoreet malesuada elit sed vestibulum. - |Etiam consequat congue fermentum. Vivamus dapibus scelerisque ipsum eu tempus. Integer non pulvinar nisi. - |Morbi ultrices sem quis nisl convallis, ac cursus nunc condimentum. Orci varius natoque penatibus et magnis dis parturient montes, - |nascetur ridiculus mus. - |""".stripMargin, - style = Map("margin" -> "32px") - ), - peopleTableCalc, - oldestPeopleChartCalc - ) - Controller(components).render().eventsIterator.lastOption + Seq( + Paragraph( + text = """ + |The spark notebooks can use the `visualise` extension method over a dataframe/dataset. It will cache the dataset by + |saving it as a file under /tmp. The `Recalculate` button refreshes the dataset (re-runs it). In this example, the + |data are random and so are different each time the `Recalculate` is pressed. + |""".stripMargin, + style = Map("margin" -> "32px") + ), + // just make it look a bit more like a proper notebook by adding some fake maths + MathJax( + expression = """ + |The following is total nonsense but it simulates some explanation that would normally be here if we had + |a proper notebook. When \(a \ne 0\), there are two solutions to \(x = {-b \pm \sqrt{b^2-4ac} \over 2a}.\) + |Aenean vel velit a lacus lacinia pulvinar. Morbi eget ex et tellus aliquam molestie sit amet eu diam. + |Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas tellus enim, tempor non efficitur et, rutrum efficitur metus. + |Nulla scelerisque, mauris sit amet accumsan iaculis, elit ipsum suscipit lorem, sed fermentum nunc purus non tellus. + |Aenean congue accumsan tempor. \(x = {-b \pm \sqrt{b^2-4ac} \over 2a}.\) maecenas vitae commodo tortor. Aliquam erat volutpat. Etiam laoreet malesuada elit sed vestibulum. + |Etiam consequat congue fermentum. Vivamus dapibus scelerisque ipsum eu tempus. Integer non pulvinar nisi. + |Morbi ultrices sem quis nisl convallis, ac cursus nunc condimentum. Orci varius natoque penatibus et magnis dis parturient montes, + |nascetur ridiculus mus. + |""".stripMargin, + style = Map("margin" -> "32px") + ), + peopleTableCalc, + oldestPeopleChartCalc + ) + Controller.noModel(components).render().run() object SparkNotebook: private val names = Array("Andy", "Kostas", "Alex", "Andreas", "George", "Jack") diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/Cached.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/Cached.scala index 3d5d1bd1..1e796372 100644 --- a/terminal21-spark/src/main/scala/org/terminal21/sparklib/Cached.scala +++ b/terminal21-spark/src/main/scala/org/terminal21/sparklib/Cached.scala @@ -49,7 +49,6 @@ class Cached[OUT: ReadWriter](val name: String, outF: => OUT)(using spark: Spark def visualize(dataUi: UiElement & HasStyle)( toUi: OUT => UiElement & HasStyle )(using - FiberExecutor, SparkSession )(using session: ConnectedSession, events: Events) = val sc = new SparkCalculation[OUT](s"spark-calc-$name", dataUi, toUi, this) From a32999448ece0df75611bbac7411843b045454c6 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 6 Mar 2024 17:28:35 +0000 Subject: [PATCH 296/313] - --- .../main/scala/org/terminal21/sparklib/Cached.scala | 1 - .../sparklib/calculations/SparkCalculation.scala | 10 ++-------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/Cached.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/Cached.scala index 1e796372..ed6d2ce4 100644 --- a/terminal21-spark/src/main/scala/org/terminal21/sparklib/Cached.scala +++ b/terminal21-spark/src/main/scala/org/terminal21/sparklib/Cached.scala @@ -1,6 +1,5 @@ package org.terminal21.sparklib -import functions.fibers.FiberExecutor import org.apache.commons.io.FileUtils import org.apache.spark.sql.SparkSession import org.terminal21.client.* diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala index e0af5171..dcb9582b 100644 --- a/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala +++ b/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala @@ -1,19 +1,13 @@ package org.terminal21.sparklib.calculations -import functions.fibers.{Fiber, FiberExecutor} -import org.apache.commons.io.FileUtils import org.apache.spark.sql.SparkSession -import org.terminal21.client.{*, given} +import org.terminal21.client.components.* import org.terminal21.client.components.UiElement.HasStyle import org.terminal21.client.components.chakra.* -import org.terminal21.client.components.* +import org.terminal21.client.* import org.terminal21.collections.TypedMap import org.terminal21.model.ClientEvent import org.terminal21.sparklib.Cached -import org.terminal21.sparklib.calculations.SparkCalculation.TriggerRedraw -import org.terminal21.sparklib.util.Environment - -import java.io.File /** A UI component that takes a spark calculation (i.e. a spark query) that results in a Dataset. It caches the results by storing them as parquet into the tmp * folder/spark-calculations/$name. Next time the calculation runs it reads the cache if available. A button should allow the user to clear the cache and rerun From 422fef2a9f727f6abc4a6be94821d5e7289ab04d Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 6 Mar 2024 17:42:55 +0000 Subject: [PATCH 297/313] - --- .../sparklib/endtoend/SparkBasics.scala | 2 +- .../org/terminal21/client/Controller.scala | 2 +- .../terminal21/client/ControllerTest.scala | 30 +++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala b/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala index 17911df0..db717804 100644 --- a/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala +++ b/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala @@ -5,7 +5,7 @@ import org.apache.spark.sql.{Dataset, SparkSession} import org.terminal21.client.components.* import org.terminal21.client.components.chakra.* import org.terminal21.client.components.nivo.* -import org.terminal21.client.{*, given} +import org.terminal21.client.* import org.terminal21.sparklib.* import org.terminal21.sparklib.endtoend.model.CodeFile import org.terminal21.sparklib.endtoend.model.CodeFile.scanSourceFiles diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 3eaa00b1..eacc0cc0 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -45,7 +45,7 @@ class RenderedController[M]( .scanLeft(initialMv): (mv, e) => val events = Events(e) val newMv = materializer(mv.model, events) - renderChanges(newMv.view) + if mv.view != newMv.view then renderChanges(newMv.view) newMv .flatMap: mv => // make sure we read the last MV change when terminating diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index eae6feba..7041dd4c 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -30,6 +30,36 @@ class ControllerTest extends AnyFunSuiteLike: seList.add(CommandEvent.sessionClosed) new Controller(it, renderChanges, materializer) + test("renderChanges() not invoked if no UI changed"): + def components(m: Int, events: Events) = + MV( + m + 1, + Box().withChildren(button, input, checkbox) + ) + + var rendered = List.empty[Seq[UiElement]] + def renderChanges(es: Seq[UiElement]) = + rendered = rendered :+ es + + newController(Seq(buttonClick, checkBoxChange), components, renderChanges).render(0).iterator.map(_.model).toList should be(List(1, 2, 3)) + rendered.size should be(1) + + test("renderChanges() invoked if UI changed"): + + def components(m: Int, events: Events) = + MV( + m + 1, + Box(text = s"m=$m").withChildren(button, input, checkbox) + ) + + var rendered = List.empty[Seq[UiElement]] + + def renderChanges(es: Seq[UiElement]) = + rendered = rendered :+ es + + newController(Seq(buttonClick, checkBoxChange), components, renderChanges).render(0).iterator.map(_.model).toList should be(List(1, 2, 3)) + rendered.size should be(3) + test("poc"): case class Person(id: Int, name: String) def personComponent(person: Person, events: Events): MV[Person] = From ada151a8f73f73f9ad70633b58a3061e30eb21c7 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 6 Mar 2024 17:54:01 +0000 Subject: [PATCH 298/313] - --- .../scala/org/terminal21/client/ControllerTest.scala | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 7041dd4c..398c37ab 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -30,6 +30,14 @@ class ControllerTest extends AnyFunSuiteLike: seList.add(CommandEvent.sessionClosed) new Controller(it, renderChanges, materializer) + test("model updated"): + def components(m: Int, events: Events) = MV(m + 1, Box()) + newController(Seq(buttonClick), components).render(0).iterator.map(_.model).toList should be(List(1, 2)) + + test("view updated"): + def components(m: Int, events: Events) = MV(m + 1, Box(text = m.toString)) + newController(Seq(buttonClick), components).render(0).iterator.map(_.view).toList should be(Seq(Seq(Box(text = "0")), Seq(Box(text = "1")))) + test("renderChanges() not invoked if no UI changed"): def components(m: Int, events: Events) = MV( @@ -41,7 +49,7 @@ class ControllerTest extends AnyFunSuiteLike: def renderChanges(es: Seq[UiElement]) = rendered = rendered :+ es - newController(Seq(buttonClick, checkBoxChange), components, renderChanges).render(0).iterator.map(_.model).toList should be(List(1, 2, 3)) + newController(Seq(buttonClick, checkBoxChange, inputChange), components, renderChanges).render(0).iterator.map(_.model).toList should be(List(1, 2, 3, 4)) rendered.size should be(1) test("renderChanges() invoked if UI changed"): From 7587fb2190352b53056bc21135183ce3f1bfaaf4 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 6 Mar 2024 17:57:16 +0000 Subject: [PATCH 299/313] - --- .../test/scala/org/terminal21/client/ControllerTest.scala | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 398c37ab..22d1756b 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -34,6 +34,11 @@ class ControllerTest extends AnyFunSuiteLike: def components(m: Int, events: Events) = MV(m + 1, Box()) newController(Seq(buttonClick), components).render(0).iterator.map(_.model).toList should be(List(1, 2)) + test("model when terminated"): + def components(m: Int, events: Events) = + MV(100, Seq(Box()), terminate = true) + newController(Seq(buttonClick), components).render(0).iterator.map(_.model).toList should be(List(100)) + test("view updated"): def components(m: Int, events: Events) = MV(m + 1, Box(text = m.toString)) newController(Seq(buttonClick), components).render(0).iterator.map(_.view).toList should be(Seq(Seq(Box(text = "0")), Seq(Box(text = "1")))) From 3aa6bb72a9ccb121e27db2663c8803359dae10e7 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 6 Mar 2024 18:03:28 +0000 Subject: [PATCH 300/313] - --- .../terminal21/client/ControllerTest.scala | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 22d1756b..58726a4e 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -73,6 +73,30 @@ class ControllerTest extends AnyFunSuiteLike: newController(Seq(buttonClick, checkBoxChange), components, renderChanges).render(0).iterator.map(_.model).toList should be(List(1, 2, 3)) rendered.size should be(3) + test("events isClicked positive"): + def components(m: Boolean, events: Events) = MV(events.isClicked(button), button) + newController(Seq(buttonClick), components).render(false).iterator.lastOption.map(_.model) should be(Some(true)) + + test("events isClicked negative"): + def components(m: Boolean, events: Events) = MV(events.isClicked(button), button) + newController(Seq(checkBoxChange), components).render(false).iterator.lastOption.map(_.model) should be(Some(false)) + + test("events changedValue positive"): + def components(m: String, events: Events) = MV(events.changedValue(input, "x"), button) + newController(Seq(inputChange), components).render("").iterator.lastOption.map(_.model) should be(Some("new-value")) + + test("events changedValue negative"): + def components(m: String, events: Events) = MV(events.changedValue(input, "x"), button) + newController(Seq(buttonClick), components).render("").iterator.lastOption.map(_.model) should be(Some("x")) + + test("events changedBooleanValue positive"): + def components(m: Boolean, events: Events) = MV(events.changedBooleanValue(checkbox, false), button) + newController(Seq(checkBoxChange), components).render(false).iterator.lastOption.map(_.model) should be(Some(true)) + + test("events changedBooleanValue negative"): + def components(m: Boolean, events: Events) = MV(events.changedBooleanValue(checkbox, false), button) + newController(Seq(buttonClick), components).render(false).iterator.lastOption.map(_.model) should be(Some(false)) + test("poc"): case class Person(id: Int, name: String) def personComponent(person: Person, events: Events): MV[Person] = From d194dfec4f5387d22d53c099fc3749601ff24a26 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Wed, 6 Mar 2024 18:08:12 +0000 Subject: [PATCH 301/313] - --- .../service/ServerSessionsServiceTest.scala | 35 ------------ .../client/ConnectedSessionTest.scala | 54 +++++-------------- 2 files changed, 12 insertions(+), 77 deletions(-) diff --git a/terminal21-server/src/test/scala/org/terminal21/server/service/ServerSessionsServiceTest.scala b/terminal21-server/src/test/scala/org/terminal21/server/service/ServerSessionsServiceTest.scala index e574b852..201327c7 100644 --- a/terminal21-server/src/test/scala/org/terminal21/server/service/ServerSessionsServiceTest.scala +++ b/terminal21-server/src/test/scala/org/terminal21/server/service/ServerSessionsServiceTest.scala @@ -1,6 +1,5 @@ package org.terminal21.server.service -import io.circe.Json import org.scalatest.funsuite.AnyFunSuiteLike import org.scalatest.matchers.should.Matchers.* import org.terminal21.model.{OnChange, OnClick, SessionClosed, SessionOptions} @@ -104,40 +103,6 @@ class ServerSessionsServiceTest extends AnyFunSuiteLike: createSession() listenerCalled should be(2) - test("changeSessionJsonState changes session's state"): - ??? -// new App: -// val session = createSession() -// val sj1 = serverJson(elements = Map("e1" -> Json.fromString("e1v"))) -// serverSessionsService.setSessionJsonState(session, sj1) -// val sj2 = serverJson(elements = Map("e2" -> Json.fromString("e2v"))) -// serverSessionsService.changeSessionJsonState(session, sj2) -// serverSessionsService.sessionStateOf(session).serverJson should be( -// sj1.include(sj2) -// ) - - test("changeSessionJsonState notifies listeners"): - ??? -// new App: -// val session = createSession() -// val sj1 = serverJson(elements = Map("e1" -> Json.fromString("e1v"))) -// serverSessionsService.setSessionJsonState(session, sj1) -// val sj2 = serverJson(elements = Map("e2" -> Json.fromString("e2v"))) -// var called = 0 -// serverSessionsService.notifyMeWhenSessionChanges: (s, sessionState, sjOption) => -// called match -// case 0 => -// s should be(session) -// sjOption should be(None) -// case 1 => -// s should be(session) -// sjOption should be(Some(sj2)) -// -// called += 1 -// true -// serverSessionsService.changeSessionJsonState(session, sj2) -// called should be(2) - test("triggerUiEvent notifies listeners for clicks"): new App: val session = createSession() diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala index f7369a8c..3eabb616 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala @@ -5,9 +5,9 @@ import org.mockito.Mockito.verify import org.scalatest.funsuite.AnyFunSuiteLike import org.scalatest.matchers.should.Matchers.* import org.terminal21.client.ConnectedSessionMock.encoder -import org.terminal21.client.components.chakra.{Box, Button, Checkbox, Editable, Input} +import org.terminal21.client.components.chakra.Editable import org.terminal21.client.components.std.{Paragraph, Span} -import org.terminal21.model.{CommandEvent, OnChange} +import org.terminal21.model.OnChange import org.terminal21.ui.std.ServerJson class ConnectedSessionTest extends AnyFunSuiteLike: @@ -29,43 +29,13 @@ class ConnectedSessionTest extends AnyFunSuiteLike: ) test("to server json"): - ??? -// val (sessionService, connectedSession) = ConnectedSessionMock.newConnectedSessionAndSessionServiceMock -// -// val p1 = Paragraph(key = "pk", text = "p1") -// val span1 = Span(key = "sk", text = "span1") -// connectedSession.render(Seq(p1.withChildren(span1))) -// connectedSession.render(Nil) -// verify(sessionService).setSessionJsonState( -// connectedSession.session, -// ServerJson( -// Seq("root"), -// Map( -// "root" -> encoder(Box("root")).deepDropNullValues, -// p1.key -> encoder(p1.withChildren()).deepDropNullValues, -// span1.key -> encoder(span1).deepDropNullValues -// ), -// Map("root" -> List(p1.key), p1.key -> Seq(span1.key), span1.key -> Nil) -// ) -// ) - - test("renderChanges changes state on server"): - ??? -// val (sessionService, connectedSession) = ConnectedSessionMock.newConnectedSessionAndSessionServiceMock -// -// val p1 = Paragraph(key = "pk", text = "p1") -// val span1 = Span(key = "sk", text = "span1") -// connectedSession.render(Seq(p1)) -// connectedSession.renderChanges(Seq(p1.withChildren(span1))) -// verify(sessionService).changeSessionJsonState( -// connectedSession.session, -// ServerJson( -// Seq("root"), -// Map( -// "root" -> encoder(Box("root")).deepDropNullValues, -// p1.key -> encoder(p1.withChildren()).deepDropNullValues, -// span1.key -> encoder(span1).deepDropNullValues -// ), -// Map("root" -> List(p1.key), p1.key -> Seq(span1.key), span1.key -> Nil) -// ) -// ) + val (sessionService, connectedSession) = ConnectedSessionMock.newConnectedSessionAndSessionServiceMock + val span1 = Span("sk", text = "span1") + val p1 = Paragraph("pk", text = "p1").withChildren(span1) + connectedSession.render(Seq(p1.withChildren(span1))) + verify(sessionService).setSessionJsonState( + connectedSession.session, + ServerJson( + Seq(encoder(p1).deepDropNullValues) + ) + ) From 7ea618a5a726973ee8e238e10693354cae0bd095 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 7 Mar 2024 11:27:32 +0000 Subject: [PATCH 302/313] - --- .../terminal21/client/ConnectedSession.scala | 4 + .../org/terminal21/client/Controller.scala | 91 +++++++++++++++++-- .../terminal21/client/ControllerTest.scala | 8 +- 3 files changed, 88 insertions(+), 15 deletions(-) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index ca6d8b9c..f05d20cc 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -72,6 +72,10 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se def fireChangeEvent(e: UiElement, newValue: String): Unit = fireEvent(CommandEvent.onChange(e, newValue)) def fireSessionClosedEvent(): Unit = fireEvent(CommandEvent.sessionClosed) + /** @return + * A new event iterator. There can be many event iterators on the same time and each of them iterates events only from after the time it was created. The + * iterator blocks while waiting to receive an event. + */ def eventIterator: Iterator[CommandEvent] = events.iterator /** Waits until at least 1 event iterator was created for the current page. Useful for testing purposes if i.e. one thread runs the main loop and gets an diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index eacc0cc0..b7c90d14 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -6,39 +6,92 @@ import org.terminal21.client.components.{OnChangeBooleanEventHandler, OnChangeEv import org.terminal21.client.components.OnClickEventHandler.CanHandleOnClickEvent import org.terminal21.model.{ClientEvent, CommandEvent, OnChange, OnClick} -type ModelViewMaterialized[M] = (M, Events) => MV[M] +type ModelViewFunction[M] = (M, Events) => MV[M] +/** Controller manages the changes in the model by receiving events. Also the rendering of the view (which is UiElements). + * + * @tparam M + * the type of the model + */ class Controller[M]( eventIteratorFactory: => Iterator[CommandEvent], renderChanges: Seq[UiElement] => Unit, - materializer: ModelViewMaterialized[M] + modelViewFunction: ModelViewFunction[M] ): + /** Sends the initialModel along with an InitialRender event to the modelViewFunction and renders the resulting UI. + * @param initialModel + * the initial state of the model + * @return + * a RenderedController. Call run() or iterator on that. + */ def render(initialModel: M): RenderedController[M] = - val mv = materializer(initialModel, Events.Empty) + val mv = modelViewFunction(initialModel, Events.Empty) renderChanges(mv.view) - new RenderedController(eventIteratorFactory, renderChanges, materializer, mv) + new RenderedController(eventIteratorFactory, renderChanges, modelViewFunction, mv) trait NoModelController: this: Controller[Unit] => def render(): RenderedController[Unit] = render(()) object Controller: - def apply[M](materializer: ModelViewMaterialized[M])(using session: ConnectedSession): Controller[M] = - new Controller(session.eventIterator, session.renderChanges, materializer) - - def noModel(component: UiElement)(using session: ConnectedSession): Controller[Unit] with NoModelController = noModel(Seq(component)) + /** Call this for a full blown model-view-controller + * @param modelViewFunction + * a function (M, Events) => MV[M] which should process the events and render Seq[UiElement] + * @param session + * the ConnectedSession + * @tparam M + * the type of the model + * @return + * Controller[M], call render(initialModel) and then iterator or run() + */ + def apply[M](modelViewFunction: ModelViewFunction[M])(using session: ConnectedSession): Controller[M] = + new Controller(session.eventIterator, session.renderChanges, modelViewFunction) + + /** Call this id you just want to render some information UI that won't receive events. + * @param component + * a single component (and it's children) to be rendered + * @param session + * ConnectedSession + * @return + * the controller. + */ + def noModel(component: UiElement)(using session: ConnectedSession): Controller[Unit] with NoModelController = noModel(Seq(component)) + + /** Call this id you just want to render some information UI that won't receive events. + * @param components + * components to be rendered + * @param session + * ConnectedSession + * @return + * the controller. + */ def noModel(components: Seq[UiElement])(using session: ConnectedSession): Controller[Unit] with NoModelController = new Controller[Unit](session.eventIterator, session.renderChanges, (_, _) => MV((), components)) with NoModelController - def noModel(materializer: Events => Seq[UiElement])(using session: ConnectedSession) = + /** Call this if you have no model but still want to receive events. I.e. a form with just an "Ok" button + * + * @param materializer + * a function that will be called initially to render the UI and whenever there is an event to render any changes to the UI + * @param session + * ConnectedSession + * @return + * the controller. + */ + def noModel(materializer: Events => Seq[UiElement])(using session: ConnectedSession): Controller[Unit] with NoModelController = new Controller[Unit](session.eventIterator, session.renderChanges, (_, events) => MV((), materializer(events))) with NoModelController class RenderedController[M]( eventIteratorFactory: => Iterator[CommandEvent], renderChanges: Seq[UiElement] => Unit, - materializer: ModelViewMaterialized[M], + materializer: ModelViewFunction[M], initialMv: MV[M] ): + /** @return + * A new event iterator. There can be many event iterators on the same time and each of them iterates events only from after the time it was created. The + * iterator blocks while waiting to receive an event. + * + * Normally a single iterator is required and most of the time it is better done by the #run() method below. + */ def iterator: EventIterator[MV[M]] = new EventIterator[MV[M]]( eventIteratorFactory .takeWhile(!_.isSessionClosed) @@ -53,8 +106,16 @@ class RenderedController[M]( .takeWhile(!_.terminate) ) + /** Gets an iterator and run the event processing. + * @return + * The last value of the model or None if the user closed the session. + */ def run(): Option[M] = iterator.lastOption.map(_.model) +/** Wraps an event and has useful methods to process it. + * @param event + * CommandEvent (like clicks, changed values, ClientEvent etc) + */ case class Events(event: CommandEvent): def isClicked(e: UiElement): Boolean = event match case OnClick(key) => key == e.key @@ -88,6 +149,16 @@ object Events: val Empty = Events(InitialRender) +/** The ModelViewFunction should return this, which contains the changes to the model, the changed view and if the event iteration should terminate. + * @param model + * the value of the model after processing the event + * @param view + * the value of the view after processing the event + * @param terminate + * if true, the event iteration will terminate + * @tparam M + * the type of the model + */ case class MV[M](model: M, view: Seq[UiElement], terminate: Boolean = false) object MV: diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala index 58726a4e..36f71d0c 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala @@ -1,14 +1,12 @@ package org.terminal21.client -import org.mockito.Mockito import org.scalatest.funsuite.AnyFunSuiteLike -import org.scalatestplus.mockito.MockitoSugar.* +import org.scalatest.matchers.should.Matchers.* import org.terminal21.client.components.UiElement import org.terminal21.client.components.chakra.* import org.terminal21.client.components.std.Input import org.terminal21.collections.SEList import org.terminal21.model.{CommandEvent, OnChange, OnClick} -import org.scalatest.matchers.should.Matchers.* class ControllerTest extends AnyFunSuiteLike: val button = Button("b1") @@ -21,14 +19,14 @@ class ControllerTest extends AnyFunSuiteLike: def newController[M]( events: Seq[CommandEvent], - materializer: ModelViewMaterialized[M], + mvFunction: ModelViewFunction[M], renderChanges: Seq[UiElement] => Unit = _ => () ): Controller[M] = val seList = SEList[CommandEvent]() val it = seList.iterator events.foreach(e => seList.add(e)) seList.add(CommandEvent.sessionClosed) - new Controller(it, renderChanges, materializer) + new Controller(it, renderChanges, mvFunction) test("model updated"): def components(m: Int, events: Events) = MV(m + 1, Box()) From b974a908510977ce11bb1c95c8f78d8e5042cb84 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 7 Mar 2024 12:37:03 +0000 Subject: [PATCH 303/313] - --- .../main/scala/tests/ChakraComponents.scala | 1 - example-scripts/bouncing-ball.sc | 12 +++- example-scripts/csv-editor.sc | 20 ++++--- example-scripts/csv-viewer.sc | 4 +- example-scripts/hello-world.sc | 5 +- example-scripts/mathjax.sc | 6 ++ example-scripts/mvc-click-form.sc | 23 ++++++-- example-scripts/mvc-user-form.sc | 16 ++++-- example-scripts/nivo-bar-chart.sc | 8 ++- example-scripts/nivo-line-chart.sc | 8 ++- example-scripts/postit.sc | 31 ++++++---- example-scripts/progress.sc | 14 ++++- example-scripts/project.scala | 2 + example-scripts/textedit.sc | 24 +++----- .../org/terminal21/client/Controller.scala | 56 +++++++++++++++++-- .../components/chakra/ChakraElement.scala | 4 ++ 16 files changed, 172 insertions(+), 62 deletions(-) diff --git a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala index f517146e..8599c988 100644 --- a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala @@ -1,7 +1,6 @@ package tests import org.terminal21.client.* -import org.terminal21.client.components.UiElement import org.terminal21.client.components.chakra.* import org.terminal21.client.components.std.Paragraph import tests.chakra.* diff --git a/example-scripts/bouncing-ball.sc b/example-scripts/bouncing-ball.sc index ab9ae53f..441938cc 100755 --- a/example-scripts/bouncing-ball.sc +++ b/example-scripts/bouncing-ball.sc @@ -18,11 +18,14 @@ Sessions .connect: session => given ConnectedSession = session + // We'll do this with an MVC approach. This is our Model: case class Ball(x: Int, y: Int, dx: Int, dy: Int): def nextPosition: Ball = val newDx = if x < 0 || x > 600 then -dx else dx val newDy = if y < 0 || y > 500 then -dy else dy Ball(x + newDx, y + newDy, newDx, newDy) + + // In order to update the ball's position, we will be sending approx 60 Ticker events per second to our controller. case object Ticker extends ClientEvent val initialModel = Ball(50, 50, 8, 8) @@ -31,18 +34,21 @@ Sessions "Files under ~/.terminal21/web will be served under /web . Please place a ball.png file under ~/.terminal21/web/images on the box where the server runs." ) + // This is our controller implementation. It takes the model (ball) and events (in this case just the Ticker which we can otherwise ignore) + // and results in the next frame's state. def components(ball: Ball, events: Events): MV[Ball] = val b = ball.nextPosition MV( b, Image(src = "/web/images/ball.png").withStyle("position" -> "fixed", "left" -> (b.x + "px"), "top" -> (b.y + "px")) ) + // We'll be sending a Ticker 60 times per second fiberExecutor.submit: while !session.isClosed do session.fireEvent(Ticker) Thread.sleep(1000 / 60) + // We are ready to create a controller instance with our components function. Controller(components) - .render(initialModel) - .iterator - .lastOption + .render(initialModel) // and render it with our initial model (it will call the components function and render any resulting UI) + .run() // and run this until the user closes the session diff --git a/example-scripts/csv-editor.sc b/example-scripts/csv-editor.sc index af21ef8b..91700aa1 100755 --- a/example-scripts/csv-editor.sc +++ b/example-scripts/csv-editor.sc @@ -2,9 +2,9 @@ // ------------------------------------------------------------------------------ // A quick and dirty csv file editor for small csv files. +// Run with : ./csv-editor -- csv-file-path // ------------------------------------------------------------------------------ -// always import these import org.terminal21.client.* import org.terminal21.client.components.* import org.terminal21.collections.TypedMapKey @@ -25,7 +25,7 @@ val fileName = args(0) val file = new File(fileName) val contents = if file.exists() then FileUtils.readFileToString(file, "UTF-8") - else "type,damage points,hit points\nmage,10dp,20hp\nwarrior,20dp,30hp" + else "type,damage points,hit points\nmage,10dp,20hp\nwarrior,20dp,30hp" // a simple csv for demo purposes val csv = toCsvModel(contents.split("\n").map(_.split(",").toSeq).toSeq) @@ -34,10 +34,10 @@ Sessions .connect: session => given ConnectedSession = session println(s"Now open ${session.uiUrl} to view the UI") - val editor = new CsvEditor(csv) + val editor = new CsvEditorPage(csv) editor.run() -/** Our model. If the user clicks "Save", we'll set `save` to true and store the csv data into `csv` +/** Our model. It stores the csv data as a Map of (x,y) coordinates -> value. */ case class CsvModel(save: Boolean, exitWithoutSave: Boolean, csv: Map[(Int, Int), String], maxX: Int, maxY: Int, status: String = "Please edit the file.") def toCsvModel(csv: Seq[Seq[String]]) = @@ -50,7 +50,10 @@ def toCsvModel(csv: Seq[Seq[String]]) = .toMap CsvModel(false, false, m, maxX, maxY) -class CsvEditor(initModel: CsvModel)(using session: ConnectedSession): +/** A nice approach to coding UI's is to create Page classes for every UI page. In this instance we need a page for our csv editor. The components function can + * be easily tested if we want to test what is rendered and how it changes the model when events occur. + */ +class CsvEditorPage(initModel: CsvModel)(using session: ConnectedSession): val saveAndExit = Button("save-exit", text = "Save & Exit") val exit = Button("exit", text = "Exit Without Saving") @@ -108,9 +111,12 @@ class CsvEditor(initModel: CsvModel)(using session: ConnectedSession): object CoordsKey extends TypedMapKey[(Int, Int)] private def newEditable(x: Int, y: Int, value: String): Editable = - Editable(s"cell-$x-$y", defaultValue = value) + Editable(s"cell-$x-$y", defaultValue = value) // note: anything receiving events should have a unique key, in this instance s"cell-$x-$y" .withChildren( EditablePreview(), EditableInput() ) - .store(CoordsKey, (x, y)) + .store( + CoordsKey, + (x, y) + ) // every UiElement has a store where we can store arbitrary data. Here we store the coordinates for the value this editable will edit diff --git a/example-scripts/csv-viewer.sc b/example-scripts/csv-viewer.sc index 89f760c0..fab68a7e 100755 --- a/example-scripts/csv-viewer.sc +++ b/example-scripts/csv-viewer.sc @@ -2,17 +2,17 @@ // ------------------------------------------------------------------------------ // A csv file viewer +// Run with: ./csv-viewer.sc -- csv-file // ------------------------------------------------------------------------------ -// always import these import org.terminal21.client.* import org.terminal21.client.components.* // use the chakra components for menus, forms etc, https://chakra-ui.com/docs/components // The scala case classes : https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala -import org.apache.commons.io.FileUtils import org.terminal21.client.components.chakra.* import java.io.File +import org.apache.commons.io.FileUtils if args.length != 1 then throw new IllegalArgumentException( diff --git a/example-scripts/hello-world.sc b/example-scripts/hello-world.sc index e2257180..f60de83d 100755 --- a/example-scripts/hello-world.sc +++ b/example-scripts/hello-world.sc @@ -1,12 +1,12 @@ #!/usr/bin/env -S scala-cli project.scala // ------------------------------------------------------------------------------ // Hello world with terminal21. +// Run with ./hello-world.sc // ------------------------------------------------------------------------------ -// always import these import org.terminal21.client.* import org.terminal21.client.components.* -// std components, https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdElement.scala +// std components like Paragraph, https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdElement.scala import org.terminal21.client.components.std.* Sessions @@ -15,4 +15,5 @@ Sessions given ConnectedSession = session Controller.noModel(Paragraph(text = "Hello World!")).render() + // since this is a read-only UI, we can exit the app but leave the session open on the UI for the user to examine the data. session.leaveSessionOpenAfterExiting() diff --git a/example-scripts/mathjax.sc b/example-scripts/mathjax.sc index 9617bc88..4a2343f5 100755 --- a/example-scripts/mathjax.sc +++ b/example-scripts/mathjax.sc @@ -1,5 +1,10 @@ #!/usr/bin/env -S scala-cli project.scala +// ------------------------------------------------------------------------------ +// Render some maths on screen for demo purposes. +// Run with ./mathjax.sc +// ------------------------------------------------------------------------------ + import org.terminal21.client.* import org.terminal21.client.components.* import org.terminal21.client.components.mathjax.* @@ -30,4 +35,5 @@ Sessions ) ) .render() + // since this is a read-only UI, we can exit the app but leave the session open on the UI for the user to examine the data. session.leaveSessionOpenAfterExiting() diff --git a/example-scripts/mvc-click-form.sc b/example-scripts/mvc-click-form.sc index 78bf9936..d4112926 100755 --- a/example-scripts/mvc-click-form.sc +++ b/example-scripts/mvc-click-form.sc @@ -1,29 +1,39 @@ #!/usr/bin/env -S scala-cli project.scala +// ------------------------------------------------------------------------------ +// MVC demo that handles a button click +// Run with ./mvc-click-form.sc +// ------------------------------------------------------------------------------ + import org.terminal21.client.* import org.terminal21.client.components.* import org.terminal21.client.components.std.* import org.terminal21.client.components.chakra.* import org.terminal21.model.SessionOptions -case class ClickForm(clicked: Boolean) - Sessions .withNewSession("mvc-click-form", "MVC form with a button") .connect: session => given ConnectedSession = session - new ClickPage(ClickForm(false)).run match + new ClickPage(ClickForm(false)).run() match case None => // the user closed the app case Some(model) => println(s"model = $model") - session.leaveSessionOpenAfterExiting() // leave the session open after exiting so that the user can examine the UI + Thread.sleep(1000) // wait a bit so that the user can see the change in the UI + +/** Our model + * + * @param clicked + * will be set to true when the button is clicked + */ +case class ClickForm(clicked: Boolean) /** One nice way to structure the code (that simplifies testing too) is to create a class for every page in the user interface. In this instance, we create a * page for the click form to be displayed. All components are in `components` method. The controller is in the `controller` method and we can run to get the * result in the `run` method. We can use these methods in unit tests to test what is rendered and how events are processed respectively. */ class ClickPage(initialForm: ClickForm)(using ConnectedSession): - def run = controller.render(initialForm).iterator.lastOption.map(_.model) + def run(): Option[ClickForm] = controller.render(initialForm).run() def components(form: ClickForm, events: Events): MV[ClickForm] = val button = Button(key = "click-me", text = "Please click me") @@ -34,7 +44,8 @@ class ClickPage(initialForm: ClickForm)(using ConnectedSession): MV( updatedForm, - Seq(msg, button) + Seq(msg, button), + terminate = updatedForm.clicked // terminate the event iteration by the controller ) def controller: Controller[ClickForm] = Controller(components) diff --git a/example-scripts/mvc-user-form.sc b/example-scripts/mvc-user-form.sc index dcd07c8a..4e63f747 100755 --- a/example-scripts/mvc-user-form.sc +++ b/example-scripts/mvc-user-form.sc @@ -5,10 +5,10 @@ import org.terminal21.client.components.* import org.terminal21.client.components.std.Paragraph import org.terminal21.client.components.chakra.* -case class UserForm( - email: String, // the email - submitted: Boolean // true if user clicks the submit button, false otherwise -) +// ------------------------------------------------------------------------------ +// MVC demo with an email form +// Run with ./mvc-user-form.sc +// ------------------------------------------------------------------------------ Sessions .withNewSession("mvc-user-form", "MVC example with a user form") @@ -20,6 +20,12 @@ Sessions case None => println("User closed session without submitting the form") +/** Our model for the form */ +case class UserForm( + email: String, // the email + submitted: Boolean // true if user clicks the submit button, false otherwise +) + /** One nice way to structure the code (that simplifies testing too) is to create a class for every page in the user interface. In this instance, we create a * page for the user form to be displayed. All components are in `components` method. The controller is in the `controller` method and we can run to get the * result in the `run` method. We can use these methods in unit tests to test what is rendered and how events are processed respectively. @@ -60,7 +66,7 @@ class UserPage(initialForm: UserForm)(using ConnectedSession): submit, output ), - terminate = events.isClicked(submit) + terminate = updatedForm.submitted // terminate the form when the submit button is clicked ) def controller: Controller[UserForm] = Controller(components) diff --git a/example-scripts/nivo-bar-chart.sc b/example-scripts/nivo-bar-chart.sc index 6b7796a0..d9b8ff01 100755 --- a/example-scripts/nivo-bar-chart.sc +++ b/example-scripts/nivo-bar-chart.sc @@ -1,5 +1,10 @@ #!/usr/bin/env -S scala-cli project.scala +// ------------------------------------------------------------------------------ +// Nivo bar chart demo, animated ! +// Run with ./nivo-bar-chart.sc +// ------------------------------------------------------------------------------ + import org.terminal21.client.* import org.terminal21.client.fiberExecutor import org.terminal21.client.components.* @@ -59,8 +64,7 @@ Sessions Controller .noModel(components) .render() - .iterator - .lastOption + .run() object NivoBarChart: def createRandomData: Seq[Seq[BarDatum]] = diff --git a/example-scripts/nivo-line-chart.sc b/example-scripts/nivo-line-chart.sc index 1acb211d..92993718 100755 --- a/example-scripts/nivo-line-chart.sc +++ b/example-scripts/nivo-line-chart.sc @@ -1,5 +1,10 @@ #!/usr/bin/env -S scala-cli project.scala +// ------------------------------------------------------------------------------ +// Nivo line chart demo, animated ! +// Run with ./nivo-line-chart.sc +// ------------------------------------------------------------------------------ + import org.terminal21.client.* import org.terminal21.client.fiberExecutor import org.terminal21.client.components.* @@ -38,8 +43,7 @@ Sessions Controller .noModel(components) .render() - .iterator - .lastOption + .run() object NivoLineChart: def createRandomData: Seq[Serie] = diff --git a/example-scripts/postit.sc b/example-scripts/postit.sc index e371fd84..d515f6e0 100755 --- a/example-scripts/postit.sc +++ b/example-scripts/postit.sc @@ -1,12 +1,12 @@ #!/usr/bin/env -S scala-cli project.scala // ------------------------------------------------------------------------------ // A note poster, where anyone can write a note +// Run with ./postit.sc // ------------------------------------------------------------------------------ -// always import these import org.terminal21.client.* import org.terminal21.client.components.* -// std components, https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdElement.scala +// std components like Paragraph, https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdElement.scala import org.terminal21.client.components.std.* // use the chakra components for menus, forms etc, https://chakra-ui.com/docs/components // The scala case classes : https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala @@ -26,23 +26,27 @@ class PostItPage(using ConnectedSession): def components(model: PostIt, events: Events): MV[PostIt] = val editor = Textarea("postit-message", placeholder = "Please post your note by clicking here and editing the content") val addButton = Button("postit", text = "Post It.") + val clearButton = Button("clear-it", text = "Clear board.") - val updatedMessages = model.messages ++ events.ifClicked(addButton, model.message) + val updatedMessages = if events.isClicked(clearButton) then Nil else model.messages ++ events.ifClicked(addButton, model.message) val updatedModel = model.copy( message = events.changedValue(editor, model.message), messages = updatedMessages ) val messagesVStack = VStack( + "the-board", align = Some("stretch"), children = updatedMessages.map: msg => - HStack().withChildren( - Image( - src = "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fa/Apple_Notes_icon.svg/2048px-Apple_Notes_icon.svg.png", - boxSize = Some("32px") - ), - Box(text = msg) - ) + HStack() + .withSpacing("8px") + .withChildren( + Image( + src = "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fa/Apple_Notes_icon.svg/2048px-Apple_Notes_icon.svg.png", + boxSize = Some("32px") + ), + Box(text = msg) + ) ) MV( updatedModel, @@ -52,7 +56,12 @@ class PostItPage(using ConnectedSession): InputLeftAddon().withChildren(EditIcon()), editor ), - addButton, + HStack() + .withSpacing("8px") + .withChildren( + addButton, + clearButton + ), messagesVStack ) ) diff --git a/example-scripts/progress.sc b/example-scripts/progress.sc index 95fea30e..fbe36888 100755 --- a/example-scripts/progress.sc +++ b/example-scripts/progress.sc @@ -1,5 +1,10 @@ #!/usr/bin/env -S scala-cli project.scala +// ------------------------------------------------------------------------------ +// Universe creation progress bar demo +// Run with ./progress.sc +// ------------------------------------------------------------------------------ + import org.terminal21.client.{*, given} import org.terminal21.client.components.* import org.terminal21.client.components.std.* @@ -8,7 +13,6 @@ import org.terminal21.model.{ClientEvent, SessionOptions} Sessions .withNewSession("universe-generation", "Universe Generation Progress") - .andOptions(SessionOptions.LeaveOpenWhenTerminated) /* leave the session tab open after terminating */ .connect: session => given ConnectedSession = session @@ -28,12 +32,18 @@ Sessions Seq(msg, progress) ) + // send a ticker to update the progress bar object Ticker extends ClientEvent fiberExecutor.submit: for _ <- 1 to 100 do Thread.sleep(200) session.fireEvent(Ticker) - Controller(components).render(1).iterator.takeWhile(_.model < 100).foreach(_ => ()) + Controller(components) + .render(1) + .iterator + .takeWhile(_.model < 100) // terminate when model == 100 + .foreach(_ => ()) // and run it // clear UI session.render(Seq(Paragraph(text = "Universe ready!"))) + session.leaveSessionOpenAfterExiting() diff --git a/example-scripts/project.scala b/example-scripts/project.scala index 37dfdb86..4cd68731 100644 --- a/example-scripts/project.scala +++ b/example-scripts/project.scala @@ -6,3 +6,5 @@ //> using dep io.github.kostaskougios::terminal21-mathjax:0.30 //> using dep commons-io:commons-io:2.15.1 + +//> using javaOpt -Xmx128m diff --git a/example-scripts/textedit.sc b/example-scripts/textedit.sc index 2741451a..292c070c 100755 --- a/example-scripts/textedit.sc +++ b/example-scripts/textedit.sc @@ -2,23 +2,20 @@ // ------------------------------------------------------------------------------ // A text file editor for small files. +// run with ./textedit.sc -- text-file // ------------------------------------------------------------------------------ import org.apache.commons.io.FileUtils - import java.io.File -// always import these import org.terminal21.client.* import org.terminal21.client.components.* -// std components, https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdElement.scala +// std components like Paragraph, https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdElement.scala import org.terminal21.client.components.std.* // use the chakra components for menus, forms etc, https://chakra-ui.com/docs/components // The scala case classes : https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala import org.terminal21.client.components.chakra.* -import java.util.concurrent.CountDownLatch - if args.length != 1 then throw new IllegalArgumentException( "Expecting 1 argument, the name of the file to edit" @@ -29,7 +26,7 @@ val file = new File(fileName) val contents = if file.exists() then FileUtils.readFileToString(file, "UTF-8") else "" -def saveFile(content: String) = +def saveFile(content: String): Unit = println(s"Saving file $fileName") FileUtils.writeStringToFile(file, content, "UTF-8") @@ -38,12 +35,11 @@ Sessions .connect: session => given ConnectedSession = session - case class Edit(content: String, savedContent: String, save: Boolean, exit: Boolean) + // the model for our editor form + case class Edit(content: String, savedContent: String, save: Boolean) // the main editor area. def components(edit: Edit, events: Events): MV[Edit] = - val editorTextArea = Textarea(key = "editor", defaultValue = edit.content) - // This will display a "saved" badge for a second when the user saves the file - // This will display an asterisk when the contents of the file are changed in the editor + val editorTextArea = Textarea("editor", defaultValue = edit.content) val saveMenu = MenuItem("save-menu", text = "Save") val exitMenu = MenuItem("exit-menu", text = "Exit") val isSave = events.isClicked(saveMenu) @@ -51,8 +47,7 @@ Sessions val updatedEdit = edit.copy( content = updatedContent, save = isSave, - savedContent = if isSave then updatedContent else edit.savedContent, - exit = events.isClicked(exitMenu) + savedContent = if isSave then updatedContent else edit.savedContent ) val modified = Badge(colorScheme = Some("red"), text = if updatedEdit.content != updatedEdit.savedContent then "*" else "") val status = Badge(text = if updatedEdit.save then "Saved" else "") @@ -78,14 +73,13 @@ Sessions ) ) - MV(updatedEdit, view) + MV(updatedEdit, view, terminate = events.isClicked(exitMenu)) println(s"Now open ${session.uiUrl} to view the UI") Controller(components) - .render(Edit(contents, contents, false, false)) + .render(Edit(contents, contents, false)) .iterator .tapEach: mv => if mv.model.save then saveFile(mv.model.content) - .takeWhile(!_.model.exit) .foreach(_ => ()) session.render(Seq(Paragraph(text = "Terminated"))) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index b7c90d14..4b66a7d9 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -121,23 +121,71 @@ case class Events(event: CommandEvent): case OnClick(key) => key == e.key case _ => false + /** If an element is clicked this results in Some(value), otherwise None + * @param e + * the element + * @param value + * the value + * @tparam V + * value type + * @return + * Some(value) if e is clicked, None if not + */ def ifClicked[V](e: UiElement & CanHandleOnClickEvent, value: => V): Option[V] = if isClicked(e) then Some(value) else None + /** @param e + * an editable element (like input) + * @param default + * the default value + * @return + * the new value of the editable (as received by an OnChange event) or the default value if the element's value didn't change + */ def changedValue(e: UiElement & OnChangeEventHandler.CanHandleOnChangeEvent, default: String): String = changedValue(e).getOrElse(default) - def changedValue(e: UiElement & OnChangeEventHandler.CanHandleOnChangeEvent): Option[String] = event match + + /** @param e + * an editable element (like input) that can receive OnChange events + * @return + * Some(newValue) if the element received an OnChange event, None if not + */ + def changedValue(e: UiElement & OnChangeEventHandler.CanHandleOnChangeEvent): Option[String] = event match case OnChange(key, value) if key == e.key => Some(value) case _ => None - def isChangedValue(e: UiElement & OnChangeEventHandler.CanHandleOnChangeEvent): Boolean = + + /** @param e + * an editable element (like input) + * @return + * true if the value of the element has changed, false if not + */ + def isChangedValue(e: UiElement & OnChangeEventHandler.CanHandleOnChangeEvent): Boolean = event match case OnChange(key, _) => key == e.key case _ => false + /** @param e + * an editable element with boolean value (like checkbox) + * @param default + * the value to return if the element wasn't changed + * @return + * the element's changed value or the default if the element didn't change + */ def changedBooleanValue(e: UiElement & OnChangeBooleanEventHandler.CanHandleOnChangeEvent, default: Boolean): Boolean = changedBooleanValue(e).getOrElse(default) - def changedBooleanValue(e: UiElement & OnChangeBooleanEventHandler.CanHandleOnChangeEvent): Option[Boolean] = event match + + /** @param e + * an editable element with boolean value (like checkbox) + * @return + * Some(value) if the element changed or None if not + */ + def changedBooleanValue(e: UiElement & OnChangeBooleanEventHandler.CanHandleOnChangeEvent): Option[Boolean] = event match case OnChange(key, value) if key == e.key => Some(value.toBoolean) case _ => None - def isChangedBooleanValue(e: UiElement & OnChangeBooleanEventHandler.CanHandleOnChangeEvent): Boolean = + + /** @param e + * an editable element with boolean value (like checkbox) + * @return + * true if the element changed or false if not + */ + def isChangedBooleanValue(e: UiElement & OnChangeBooleanEventHandler.CanHandleOnChangeEvent): Boolean = event match case OnChange(key, _) => key == e.key case _ => false diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala index 6049a2f7..23bddb4d 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala @@ -121,7 +121,9 @@ case class HStack( override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withSpacing(v: Option[String]) = copy(spacing = v) + def withSpacing(v: String) = copy(spacing = Some(v)) def withAlign(v: Option[String]) = copy(align = v) + def withAlign(v: String) = copy(align = Some(v)) override def withDataStore(ds: TypedMap) = copy(dataStore = ds) case class VStack( @@ -138,7 +140,9 @@ case class VStack( override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) def withSpacing(v: Option[String]) = copy(spacing = v) + def withSpacing(v: String) = copy(spacing = Some(v)) def withAlign(v: Option[String]) = copy(align = v) + def withAlign(v: String) = copy(align = Some(v)) override def withDataStore(ds: TypedMap) = copy(dataStore = ds) case class SimpleGrid( From 2606ee1c9ec1e5e89ecb1941f74b08036703d3a8 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 7 Mar 2024 14:45:20 +0000 Subject: [PATCH 304/313] - --- .../serverapp/bundled/ServerStatusApp.scala | 34 +++++++++++++++++-- .../org/terminal21/client/Controller.scala | 4 +++ .../components/chakra/ChakraElement.scala | 3 ++ .../client/components/chakra/QuickTabs.scala | 5 +-- 4 files changed, 41 insertions(+), 5 deletions(-) diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala index 875c0a52..89b2ab26 100644 --- a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala +++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala @@ -1,6 +1,7 @@ package org.terminal21.serverapp.bundled import functions.fibers.FiberExecutor +import io.circe.Json import org.terminal21.client.* import org.terminal21.client.components.* import org.terminal21.client.components.chakra.* @@ -107,14 +108,41 @@ class ServerStatusPage( class ViewServerStatePage(using session: ConnectedSession): + private val firstStyle = Map("margin" -> "24px") + def jsonToTable(depth: Int, j: Json): UiElement = + j.fold( + jsonNull = Text(text = "null"), + jsonBoolean = b => Text(text = b.toString), + jsonNumber = n => Text(text = n.toString), + jsonString = s => Text(text = s), + jsonArray = arr => if arr.isEmpty then Text(text = "") else Box().withChildren(arr.map(a => jsonToTable(depth + 1, a))*), + jsonObject = o => { + val keyValues = o.toList + if keyValues.isEmpty then Text(text = "") + else + Table(style = if depth == 1 then firstStyle else Map.empty).withChildren( + Tbody(children = keyValues.map: (k, v) => + Tr() + .withBg("blackAlpha.500") + .withChildren( + Td(text = k), + Td().withChildren(jsonToTable(depth + 1, v)) + )) + ) + } + ) + def runFor(state: SessionState): Unit = val sj = state.serverJson val components = Seq( QuickTabs() - .withTabs("Json") - .withTabPanels( - Seq(Paragraph(text = sj.elements.toString)) + .withTabs("Hierarchy", "Json") + .withTabPanelsSimple( + Box().withChildren(sj.elements.map(j => jsonToTable(1, j))*), + Paragraph().withChildren( + sj.elements.map(e => Text(text = e.toString))* + ) ) ) session.render(components) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala index 4b66a7d9..1c6e8d4d 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala @@ -6,6 +6,10 @@ import org.terminal21.client.components.{OnChangeBooleanEventHandler, OnChangeEv import org.terminal21.client.components.OnClickEventHandler.CanHandleOnClickEvent import org.terminal21.model.{ClientEvent, CommandEvent, OnChange, OnClick} +/** The initial function passed on to a controller in order to create the MVC iterator. + * @tparam M + * the type of the model + */ type ModelViewFunction[M] = (M, Events) => MV[M] /** Controller manages the changes in the model by receiving events. Also the rendering of the view (which is UiElements). diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala index 23bddb4d..6705d250 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala @@ -1771,6 +1771,7 @@ case class Tfoot(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, sty case class Tr( key: String = Keys.nextKey, children: Seq[UiElement] = Nil, + bg: Option[String] = None, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty ) extends ChakraElement @@ -1779,6 +1780,8 @@ case class Tr( override def withChildren(cn: UiElement*) = copy(children = cn) override def withStyle(v: Map[String, Any]) = copy(style = v) def withKey(v: String) = copy(key = v) + def withBg(v: Option[String]) = copy(bg = v) + def withBg(v: String) = copy(bg = Some(v)) override def withDataStore(ds: TypedMap) = copy(dataStore = ds) case class Th( diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala index fe5816f1..90c4f963 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala @@ -14,8 +14,9 @@ case class QuickTabs( with HasStyle: type This = QuickTabs - def withTabs(tabs: String | Seq[UiElement]*): QuickTabs = copy(tabs = tabs) - def withTabPanels(tabPanels: Seq[UiElement]*): QuickTabs = copy(tabPanels = tabPanels) + def withTabs(tabs: String | Seq[UiElement]*): QuickTabs = copy(tabs = tabs) + def withTabPanels(tabPanels: Seq[UiElement]*): QuickTabs = copy(tabPanels = tabPanels) + def withTabPanelsSimple(tabPanels: UiElement*): QuickTabs = copy(tabPanels = tabPanels.map(e => Seq(e))) override lazy val rendered = Seq( From 6864a495b25ade9e88a8488e9e1e8c66992efec7 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 7 Mar 2024 15:05:05 +0000 Subject: [PATCH 305/313] - --- Readme.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/Readme.md b/Readme.md index 663eb464..61778d54 100644 --- a/Readme.md +++ b/Readme.md @@ -79,23 +79,24 @@ Let's create a simple hello world script in scala-cli that uses terminal21 serve ```scala import org.terminal21.client.* import org.terminal21.client.components.* +// std components like Paragraph, https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdElement.scala +import org.terminal21.client.components.std.* -Sessions.withNewSession("hello-world", "Hello World Example"): session => +Sessions + .withNewSession("hello-world", "Hello World Example") + .connect: session => given ConnectedSession = session - Seq( - Paragraph(text = "Hello World!") - ).render() - session.waitTillUserClosesSession() + + Controller.noModel(Paragraph(text = "Hello World!")).render() + // since this is a read-only UI, we can exit the app but leave the session open on the UI for the user to examine the data. + session.leaveSessionOpenAfterExiting() ``` If we run this, then we can point our browser to the server, and we will see this UI: ![hello world ui](docs/images/hello-world.png) -The script will wait until the user clicks the close button, which then will invalidate the -session it has with the server and terminate the app. - -![hello world ui](docs/images/hello-world-terminated.png) +The script will wait until the user clicks the close button and then the script will terminate. # Usecases From 4162349effa64fe2c7877f3e91b85eb441ecb920 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 7 Mar 2024 15:08:58 +0000 Subject: [PATCH 306/313] - --- docs/quick.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/quick.md b/docs/quick.md index 616e0709..38e2a37d 100644 --- a/docs/quick.md +++ b/docs/quick.md @@ -32,3 +32,17 @@ QuickTabs() ``` +## QuickFormControl + +Simplifies creating forms. + +```scala +QuickFormControl() + .withLabel("Email address") + .withHelperText("We'll never share your email.") + .withInputGroup( + InputLeftAddon().withChildren(EmailIcon()), + emailInput, + InputRightAddon().withChildren(CheckCircleIcon()) + ) +``` \ No newline at end of file From 6926a68d4dfdc39e8b24a27be5bcef779455c588 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 7 Mar 2024 15:13:20 +0000 Subject: [PATCH 307/313] - --- Readme.md | 2 +- docs/std.md | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 61778d54..5b1e8148 100644 --- a/Readme.md +++ b/Readme.md @@ -123,7 +123,7 @@ can be used for things like: # Available UI Components -Standard html elements +Standard html elements like paragraphs, headers, cookies etc [Std](docs/std.md) Generic components for buttons, menus, forms, text, grids, tables: diff --git a/docs/std.md b/docs/std.md index c08ba513..b0798a06 100644 --- a/docs/std.md +++ b/docs/std.md @@ -30,4 +30,19 @@ Header1(text = "Welcome to the std components demo/test") val output = Paragraph(text = "This will reflect what you type in the input") input.onChange: newValue => output.withText(newValue).renderChanges() +``` + +### Cookies + +Set a cookie: + +```scala +Cookie(name = "cookie-name", value = "cookie value") +``` + +Read a cookie: + +```scala +val cookieReader = CookieReader(key = "cookie-reader", name = "cookie-name") +val cookieValue = events.changedValue(cookieReader) ``` \ No newline at end of file From 19d6b849b4c34a64c48ccea7de21680e5c07553d Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 7 Mar 2024 16:06:13 +0000 Subject: [PATCH 308/313] - --- Readme.md | 8 ++++++++ docs/run-on-server.md | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 docs/run-on-server.md diff --git a/Readme.md b/Readme.md index 5b1e8148..bc93c370 100644 --- a/Readme.md +++ b/Readme.md @@ -140,6 +140,14 @@ Maths: Spark: [Spark](docs/spark.md) + +# Apps running on server + +User applications can run on the terminal21 server so that they are always available. The api is the same but a bit of extra wiring is required +for the terminal21 server to be able to use them. + +See [running apps on the server][docs/run-on-server.md] + # Architecture Terminal21 consist of : diff --git a/docs/run-on-server.md b/docs/run-on-server.md new file mode 100644 index 00000000..30ea491b --- /dev/null +++ b/docs/run-on-server.md @@ -0,0 +1,35 @@ +# Running applications on the server + +To create an app that runs on the server, implement the `ServerSideApp` trait and then pass your implementation to the `start()` method of the server: + +```scala +class MyServerApp extends ServerSideApp: + override def name = "My Server App" + + override def description = "Some app that I want to be available when I start the server" + + override def createSession(serverSideSessions: ServerSideSessions, dependencies: Dependencies): Unit = + serverSideSessions + .withNewSession("my-server-app-session", name) + .connect: session => + given ConnectedSession = session + ... your app code ... +``` + +See for example the [default terminal21 apps](../terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled). + +Now make sure your app is included in the server's classpath and then pass it as an argument to `start()`, i.e. with this `scala-cli` script: + +```scala +//> using jvm "21" +//> using scala 3 +//> using javaOpt -Xmx128m +//> using dep io.github.kostaskougios::terminal21-server-app:$VERSION +//> using dep MY_APP_DEP + +import org.terminal21.server.Terminal21Server + +Terminal21Server.start(apps=Seq(new MyServerApp)) +``` + +Now start the server and the app should be available in the app list of terminal21. From 254f389ce1435a65db5d592aaa6829976a3c9bd9 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 7 Mar 2024 18:42:42 +0000 Subject: [PATCH 309/313] - --- docs/run-on-server.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/docs/run-on-server.md b/docs/run-on-server.md index 30ea491b..74e4a968 100644 --- a/docs/run-on-server.md +++ b/docs/run-on-server.md @@ -21,15 +21,12 @@ See for example the [default terminal21 apps](../terminal21-server-app/src/main/ Now make sure your app is included in the server's classpath and then pass it as an argument to `start()`, i.e. with this `scala-cli` script: ```scala -//> using jvm "21" -//> using scala 3 -//> using javaOpt -Xmx128m -//> using dep io.github.kostaskougios::terminal21-server-app:$VERSION +//> ... //> using dep MY_APP_DEP import org.terminal21.server.Terminal21Server -Terminal21Server.start(apps=Seq(new MyServerApp)) +Terminal21Server.start(apps = Seq(new MyServerApp)) ``` Now start the server and the app should be available in the app list of terminal21. From 62567cdf09047703ef8dbe03d78ad49a0bcf0e45 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 7 Mar 2024 19:48:08 +0000 Subject: [PATCH 310/313] - --- docs/tutorial.md | 387 ++++++++++++++++++------------ example-scripts/hello-world.sc | 2 +- example-scripts/mvc-click-form.sc | 2 +- example-scripts/mvc-user-form.sc | 16 +- example-scripts/progress.sc | 2 +- 5 files changed, 250 insertions(+), 159 deletions(-) diff --git a/docs/tutorial.md b/docs/tutorial.md index a6742422..701f0d32 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -9,7 +9,8 @@ For a glimpse on what can be done with terminal21, please have a look at the [te Terminal21 is not meant as a way to create websites. It is rather meant to give UI's to the odd jobs that has to be performed by scripts and where it would require a lot of effort to create a dedicated web server with a UI. It is perfect for scripting for i.e. those internal odd tasks that have to be performed at your workplace or even for things you would -like to do on your box. And you won't have to write a single line of html or javascript. +like to do on your box or even maybe to present some code of yours running with a UI rather than a powerpoint +presentation. And you won't have to write a single line of html or javascript. This tutorial will use `scala-cli` but the same applies for `sbt` or `mill` projects that use the terminal21 libraries. If you have `scala-cli` installed on your box, you're good to go, there are no other requirements to run terminal21 scripts. Jdk and @@ -19,7 +20,7 @@ All example code is under `example-scripts` of this repo, feel free to checkout ## Starting the terminal21 server -The easiest way to start the terminal21 server is to have a `scala-cli` script on the box where the server will run. +The easiest way to start the terminal21 server is to have a `scala-cli` script on the box where the server will run: [server.sc](../example-scripts/server.sc) @@ -61,11 +62,14 @@ To do this we can create a [hello-world.sc](../example-scripts/hello-world.sc) i ```scala #!/usr/bin/env -S scala-cli project.scala +// ------------------------------------------------------------------------------ +// Hello world with terminal21. +// Run with ./hello-world.sc +// ------------------------------------------------------------------------------ -// always import these import org.terminal21.client.* import org.terminal21.client.components.* -// std components, https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdElement.scala +// std components like Paragraph, https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdElement.scala import org.terminal21.client.components.std.* Sessions @@ -73,7 +77,8 @@ Sessions .connect: session => given ConnectedSession = session - Paragraph(text = "Hello World!").render() + Controller.noModel(Paragraph(text = "Hello World!")).render() + // since this is a read-only UI, we can exit the app but leave the session open for the user to examine the page. session.leaveSessionOpenAfterExiting() ``` @@ -102,15 +107,16 @@ Sessions ![hello-world](images/hello-world.png) -Next is the actual user interface, in this example just a paragraph with a "Hello World!": +Next is the actual user interface, in this example just a paragraph with a "Hello World!". In order for it to be rendered, we +quickly construct a Controller (terminal21 uses an MVC architecture idiomatic to scala, more on this later on): ```scala -Paragraph(text = "Hello World!").render() +Controller.noModel(Paragraph(text = "Hello World!")).render() ``` -The `render()` method sends the UI to the server which in turn sends it to the terminal21 UI so that it is rendered. +The `render()` method sends the UI components to the server which in turn sends it to the terminal21 react frontend so that it is rendered. -Finally because this is just a presentation script (we don't expect any feedback from the user), we can terminate it but +Finally, because this is just a presentation script (we don't expect any feedback from the user), we can terminate it but inform terminal21 we want to leave the session open so that the user has a chance to see it. ```scala @@ -132,83 +138,135 @@ the progress bar and also give an informative message regarding which stage of t ```scala #!/usr/bin/env -S scala-cli project.scala -import org.terminal21.client.* +// ------------------------------------------------------------------------------ +// Universe creation progress bar demo +// Run with ./progress.sc +// ------------------------------------------------------------------------------ + +import org.terminal21.client.{*, given} import org.terminal21.client.components.* import org.terminal21.client.components.std.* import org.terminal21.client.components.chakra.* -import org.terminal21.model.SessionOptions +import org.terminal21.model.{ClientEvent, SessionOptions} Sessions .withNewSession("universe-generation", "Universe Generation Progress") - .andOptions(SessionOptions.LeaveOpenWhenTerminated) /* leave the session tab open after terminating */ .connect: session => given ConnectedSession = session - val msg = Paragraph(text = "Generating universe ...") - val progress = Progress(value = 1) - - Seq(msg, progress).render() - - for i <- 1 to 100 do - val p = progress.withValue(i) - val m = - if i < 10 then msg - else if i < 30 then msg.withText("Creating atoms") - else if i < 50 then msg.withText("Big bang!") - else if i < 80 then msg.withText("Inflating") - else msg.withText("Life evolution") - - Seq(p, m).renderChanges() - Thread.sleep(100) - + def components(model: Int, events: Events): MV[Int] = + val status = + if model < 10 then "Generating universe ..." + else if model < 30 then "Creating atoms" + else if model < 50 then "Big bang!" + else if model < 80 then "Inflating" + else "Life evolution" + + val msg = Paragraph(text = status) + val progress = Progress(value = model) + + MV( + model + 1, + Seq(msg, progress) + ) + + // send a ticker to update the progress bar + object Ticker extends ClientEvent + fiberExecutor.submit: + for _ <- 1 to 100 do + Thread.sleep(200) + session.fireEvent(Ticker) + + Controller(components) + .render(1) + .iterator + .takeWhile(_.model < 100) // terminate when model == 100 + .foreach(_ => ()) // and run it // clear UI - session.clear() - Paragraph(text = "Universe ready!").render() + session.render(Seq(Paragraph(text = "Universe ready!"))) + session.leaveSessionOpenAfterExiting() ``` -Here we create a paragraph and a progress bar. - +We start by declaring our components into a function: ```scala - val msg = Paragraph(text = "Generating universe ...") - val progress = Progress(value = 1) +def components(model: Int, events: Events): MV[Int] ``` +This kind of function is the standard way to create reusable UI components in terminal21. It takes the model (the progress so far as an Int between 0 and 100), +`Events` which holds any event that was received and returns with a model-view class `MV[Int]` because our model is an `Int`. The top-level component of a page +must have this signature (there are variations but it has to return an `MV`) but sub-components can be any functions with any number of arguments or return type. More on that later. -Then we render them for the first time on screen. When we want to add a new element to the UI, we use the `render()` method. When -we want to update an existing element we use the `renderChanges()` method. +We then create a paragraph and a progress bar. ```scala - Seq(msg, progress).render() + val msg = Paragraph(text = status) + val progress = Progress(value = model) ``` -Then we have our main loop where the calculations occur. We just use a `Thread.sleep` to simulate that some important task is being calculated. And we -update the progress bar and the message in our paragraph. +Finally, we return the changed model and view: + ```scala -val p = progress.withValue(i) -val m = ... msg.withText("Creating atoms") ... + MV( + model + 1, + Seq(msg, progress) + ) ``` -Note the `e.withX()` methods. Those help us change a value on a UI element. We get a copy of the UI element which we can render as an update: +Ok we got our component, but how does it know when to update the progress and increase the model by 1? +For that we need to send it a custom event. The `components` function is called once when we call the +`render()` method on the controller and once for each event received. Since we don't have any UI component +that may send an event, we will send it ourselfs in a separate fiber: ```scala -Seq(p, m).renderChanges() +// send a ticker to update the progress bar +object Ticker extends ClientEvent +fiberExecutor.submit: + for _ <- 1 to 100 do + Thread.sleep(200) + session.fireEvent(Ticker) ``` -Finally, when the universe is ready, we just clear the UI and render a paragraph before we exit. +Remember the `events: Events` in our `components` function? This will contain the `Ticker` event, but it is of no use, so +the components function ignores it. + +Now we can create the `Controller` and iterate through all events: ```scala -session.clear() -Paragraph(text = "Universe ready!").render() +Controller(components) + .render(1) // render takes the initial model value, in this case our model is the progress as an Int between 0 and 100. We start with 1 and increment it in the components function + .iterator // this is a blocking iterator with events. If there is no event it will block. + .takeWhile(_.model < 100) // terminate when model == 100 + .foreach(_ => ()) // and run it ``` + +Thats it. We have a progress bar that displays different messages depending on the stage of our universe creation. And our code would +also be easily testable. The `components` is just a function that returns the model and the UI components, so we can easily assert what +is rendered based on the model value and if the model is updated correctly based on events. A nicer way to structure each page of +our user interface would be to have it in a class with a `components` and a `controller` function. That class would be easily testable, +see the following classes if you would like to find out more. It is an app with 2 pages, a login and loggedin page: + +[LoginPage & LoggedInPage](../end-to-end-tests/src/main/scala/tests/LoginPage.scala) + +[LoginPageTest](../end-to-end-tests/src/test/scala/tests/LoginPageTest.scala) + +[LoggedInTest](../end-to-end-tests/src/test/scala/tests/LoggedInTest.scala) + ## Handling clicks -Some UI elements allow us to attach an `onClick` handler. When the user clicks the element, our scala code runs. +Some UI elements like `Button` are clickable. When the user clicks the element, our controller gets an OnClick event. -Let's see for example [on-click.sc](../example-scripts/on-click.sc). We will create a paragraph and a button. When the +Let's see for example [mvc-click-form.sc](../example-scripts/mvc-click-form.sc). We will create a paragraph and a button. When the user clicks the button, the paragraph text will change and the script will exit. +We will create the Page class we mentioned previously, makes it more structured and easier to test. + ```scala #!/usr/bin/env -S scala-cli project.scala +// ------------------------------------------------------------------------------ +// MVC demo that handles a button click +// Run with ./mvc-click-form.sc +// ------------------------------------------------------------------------------ + import org.terminal21.client.* import org.terminal21.client.components.* import org.terminal21.client.components.std.* @@ -216,104 +274,88 @@ import org.terminal21.client.components.chakra.* import org.terminal21.model.SessionOptions Sessions - .withNewSession("on-click-example", "On Click Handler") - .andOptions(SessionOptions.LeaveOpenWhenTerminated) + .withNewSession("mvc-click-form", "MVC form with a button") .connect: session => given ConnectedSession = session - - @volatile var exit = false - val msg = Paragraph(text = "Waiting for user to click the button") - val button = Button(text = "Please click me").onClick: () => - msg.withText("Button clicked.").renderChanges() - exit = true - - Seq(msg, button).render() - - session.waitTillUserClosesSessionOr(exit) + new ClickPage(ClickForm(false)).run() match + case None => // the user closed the app + case Some(model) => println(s"model = $model") + + Thread.sleep(1000) // wait a bit so that the user can see the change in the UI + +/** Our model + * + * @param clicked + * will be set to true when the button is clicked + */ +case class ClickForm(clicked: Boolean) + +/** One nice way to structure the code (that simplifies testing too) is to create a class for every page in the user interface. In this instance, we create a + * page for the click form to be displayed. All components are in `components` method. The controller is in the `controller` method and we can run to get the + * result in the `run` method. We can use these methods in unit tests to test what is rendered and how events are processed respectively. + */ +class ClickPage(initialForm: ClickForm)(using ConnectedSession): + def run(): Option[ClickForm] = controller.render(initialForm).run() + + def components(form: ClickForm, events: Events): MV[ClickForm] = + val button = Button(key = "click-me", text = "Please click me") + val updatedForm = form.copy( + clicked = events.isClicked(button) + ) + val msg = Paragraph(text = if updatedForm.clicked then "Button clicked!" else "Waiting for user to click the button") + + MV( + updatedForm, + Seq(msg, button), + terminate = updatedForm.clicked // terminate the event iteration + ) + + def controller: Controller[ClickForm] = Controller(components) ``` -First we create the paragraph and button. We attach an `onClick` handler on the button: +We create the paragraph and button. Components like the `Button` that receive events must have a unique key, so we set that to "click-me": ```scala - val button = Button(text = "Please click me").onClick: () => - msg.withText("Button clicked.").renderChanges() - exit = true +val button = Button(key = "click-me", text = "Please click me") +val msg = Paragraph(text = if updatedForm.clicked then "Button clicked!" else "Waiting for user to click the button") ``` -Here we change the paragraph text and also update `exit` to `true`. -Our script waits until var `exit` becomes true and then terminates. +If the button is clicked, we update our model accordingly: ```scala -session.waitTillUserClosesSessionOr(exit) +val updatedForm = form.copy( + clicked = events.isClicked(button) +) ``` -Now if we run it with `./on-click.sc` and click the button, the script will terminate. - -## Reading updated values - -Some UI element values, like input boxes, can be changed by the user. We can read the changed value at any point of our -code or install an onChange handler so that we read the value as soon as the user changes it. - -Let's see how we can just read the value. The following script will create an email input box and a button. Whenever -the button is pressed, it will read the email and create a new paragraph with the email value. - -[read-changed-value.sc](../example-scripts/read-changed-value.sc) - -![read-value](images/tutorial/read-value.png) +Finally we return the `MV` with our model and view. Note that we inform the controller we want to terminate the event iteration when the button is clicked: ```scala -#!/usr/bin/env -S scala-cli project.scala - -import org.terminal21.client.* -import org.terminal21.client.components.* -import org.terminal21.client.components.std.Paragraph -import org.terminal21.client.components.chakra.* - -Sessions - .withNewSession("read-changed-value-example", "Read Changed Value") - .connect: session => - given ConnectedSession = session - - val email = Input(`type` = "email", value = "my@email.com") - val output = Box() - - Seq( - FormControl().withChildren( - FormLabel(text = "Email address"), - InputGroup().withChildren( - InputLeftAddon().withChildren(EmailIcon()), - email - ), - FormHelperText(text = "We'll never share your email.") - ), - Button(text = "Read Value").onClick: () => - val value = email.current.value - output.current.addChildren(Paragraph(text = s"The value now is $value")).renderChanges() - , - output - ).render() - - session.waitTillUserClosesSession() +MV( + updatedForm, + Seq(msg, button), + terminate = updatedForm.clicked // terminate the event iteration +) ``` -The important bit is this: - +We are good now to run our page: ```scala - Button(text = "Read Value").onClick: () => - val value = email.current.value - output.current.addChildren(Paragraph(text = s"The value now is $value")).renderChanges() + def run(): Option[ClickForm] = controller.render(initialForm).run() ``` -When the button is clicked, we get the current state of the `email` input box via `email.current`. And then get it's value, `email.current.value`. +The controller renders the form with an initial model of `initialForm`. This effectively just calls our `def components(form: ClickForm, events: Events)` with +an `InitialRender` event and form=initialForm. And then sends the resulting view to the terminal21 server. + +Now if we run it with `./on-click.sc` and click the button, the script will terminate with an updated message in the paragraph. -Also in order to append a new paragraph to the `output`, we get the current state of it (which includes any previous paragraphs we have added) and then -add a paragraph as a new child. Then we render the changes of `output` which includes the paragraphs. +## Reading updated values -We can now give it a try: `./read-changed-value.sc` +Some UI element values, like input boxes, can be changed by the user. We can read the changed value and update our model accordingly. -We can also add an `onChange` event handler on our input box and get the value whenever the user changes it. +Lets create a form with an inputbox where the user can enter his/her email and a submit button. Lets follow our Page & Form class approach, it may make +our code a bit longer but also more structured and easier to test. -[on-change.sc](../example-scripts/on-change.sc) +[mvc-user-form.sc](../example-scripts/mvc-user-form.sc) ```scala #!/usr/bin/env -S scala-cli project.scala @@ -323,38 +365,87 @@ import org.terminal21.client.components.* import org.terminal21.client.components.std.Paragraph import org.terminal21.client.components.chakra.* +// ------------------------------------------------------------------------------ +// MVC demo with an email form +// Run with ./mvc-user-form.sc +// ------------------------------------------------------------------------------ + Sessions - .withNewSession("on-change-example", "On Change event handler") + .withNewSession("mvc-user-form", "MVC example with a user form") .connect: session => given ConnectedSession = session - - val output = Paragraph(text = "Please modify the email.") - val email = Input(`type` = "email", value = "my@email.com").onChange: v => - output.withText(s"Email value : $v").renderChanges() - - Seq( - FormControl().withChildren( - FormLabel(text = "Email address"), - InputGroup().withChildren( - InputLeftAddon().withChildren(EmailIcon()), - email - ), - FormHelperText(text = "We'll never share your email.") + new UserPage(UserForm("my@email.com", false)).run match + case Some(submittedUser) => + println(s"Submitted: $submittedUser") + case None => + println("User closed session without submitting the form") + +/** Our model for the form */ +case class UserForm( + email: String, // the email + submitted: Boolean // true if user clicks the submit button, false otherwise +) + +/** One nice way to structure the code (that simplifies testing too) is to create a class for every page in the user interface. In this instance, we create a + * page for the user form to be displayed. All components are in `components` method. The controller is in the `controller` method and we can run to get the + * result in the `run` method. We can use these methods in unit tests to test what is rendered and how events are processed respectively. + */ +class UserPage(initialForm: UserForm)(using ConnectedSession): + + /** Runs the form and returns the results + * @return + * if None, the user didn't submit the form (i.e. closed the session), if Some(userForm) the user submitted the form. + */ + def run: Option[UserForm] = + controller.render(initialForm).run().filter(_.submitted) + + /** @return + * all the components that should be rendered for the page + */ + def components(form: UserForm, events: Events): MV[UserForm] = + val emailInput = Input(key = "email", `type` = "email", defaultValue = initialForm.email) + val submitButton = Button(key = "submit", text = "Submit") + + val updatedForm = form.copy( + email = events.changedValue(emailInput, form.email), + submitted = events.isClicked(submitButton) + ) + + val output = Paragraph(text = if events.isChangedValue(emailInput) then s"Email changed: ${updatedForm.email}" else "Please modify the email.") + + MV( + updatedForm, + Seq( + QuickFormControl() + .withLabel("Email address") + .withInputGroup( + InputLeftAddon().withChildren(EmailIcon()), + emailInput + ) + .withHelperText("We'll never share your email."), + submitButton, + output ), - output - ).render() - - session.waitTillUserClosesSession() + terminate = updatedForm.submitted // terminate the form when the submit button is clicked + ) + + def controller: Controller[UserForm] = Controller(components) ``` -The important bit are these lines: +The important bit is here: ```scala - val output = Paragraph(text = "Please modify the email.") - val email = Input(`type` = "email", value = "my@email.com").onChange: v => - output.withText(s"Email value : $v").renderChanges() +val emailInput = Input(key = "email", `type` = "email", defaultValue = initialForm.email) +val submitButton = Button(key = "submit", text = "Submit") + +val updatedForm = form.copy( + email = events.changedValue(emailInput, form.email), + submitted = events.isClicked(submitButton) +) + +val output = Paragraph(text = if events.isChangedValue(emailInput) then s"Email changed: ${updatedForm.email}" else "Please modify the email.") ``` -For the `Input` box, we add an `onChange` handler that gets the new value as `v`. We then use the value to update the paragraph. +When we update the model, we set `email = events.changedValue(emailInput, form.email)`. If the event was an `OnChange` event for our `emailInput`, this will set the email +to the changed value. If not it will revert back to the `form.email`, effectively leaving the email unchanged. -This script can be run as `./on-change.sc`. diff --git a/example-scripts/hello-world.sc b/example-scripts/hello-world.sc index f60de83d..b191660f 100755 --- a/example-scripts/hello-world.sc +++ b/example-scripts/hello-world.sc @@ -15,5 +15,5 @@ Sessions given ConnectedSession = session Controller.noModel(Paragraph(text = "Hello World!")).render() - // since this is a read-only UI, we can exit the app but leave the session open on the UI for the user to examine the data. + // since this is a read-only UI, we can exit the app but leave the session open for the user to examine the page. session.leaveSessionOpenAfterExiting() diff --git a/example-scripts/mvc-click-form.sc b/example-scripts/mvc-click-form.sc index d4112926..e014c354 100755 --- a/example-scripts/mvc-click-form.sc +++ b/example-scripts/mvc-click-form.sc @@ -45,7 +45,7 @@ class ClickPage(initialForm: ClickForm)(using ConnectedSession): MV( updatedForm, Seq(msg, button), - terminate = updatedForm.clicked // terminate the event iteration by the controller + terminate = updatedForm.clicked // terminate the event iteration ) def controller: Controller[ClickForm] = Controller(components) diff --git a/example-scripts/mvc-user-form.sc b/example-scripts/mvc-user-form.sc index 4e63f747..d8b92d0f 100755 --- a/example-scripts/mvc-user-form.sc +++ b/example-scripts/mvc-user-form.sc @@ -37,21 +37,21 @@ class UserPage(initialForm: UserForm)(using ConnectedSession): * if None, the user didn't submit the form (i.e. closed the session), if Some(userForm) the user submitted the form. */ def run: Option[UserForm] = - controller.render(initialForm).iterator.lastOption.map(_.model).filter(_.submitted) + controller.render(initialForm).run().filter(_.submitted) /** @return * all the components that should be rendered for the page */ def components(form: UserForm, events: Events): MV[UserForm] = - val email = Input(key = "email", `type` = "email", defaultValue = initialForm.email) - val submit = Button(key = "submit", text = "Submit") + val emailInput = Input(key = "email", `type` = "email", defaultValue = initialForm.email) + val submitButton = Button(key = "submit", text = "Submit") val updatedForm = form.copy( - email = events.changedValue(email, form.email), - submitted = events.isClicked(submit) + email = events.changedValue(emailInput, form.email), + submitted = events.isClicked(submitButton) ) - val output = Paragraph(text = if events.isChangedValue(email) then s"Email changed: ${updatedForm.email}" else "Please modify the email.") + val output = Paragraph(text = if events.isChangedValue(emailInput) then s"Email changed: ${updatedForm.email}" else "Please modify the email.") MV( updatedForm, @@ -60,10 +60,10 @@ class UserPage(initialForm: UserForm)(using ConnectedSession): .withLabel("Email address") .withInputGroup( InputLeftAddon().withChildren(EmailIcon()), - email + emailInput ) .withHelperText("We'll never share your email."), - submit, + submitButton, output ), terminate = updatedForm.submitted // terminate the form when the submit button is clicked diff --git a/example-scripts/progress.sc b/example-scripts/progress.sc index fbe36888..aad7f8c5 100755 --- a/example-scripts/progress.sc +++ b/example-scripts/progress.sc @@ -40,7 +40,7 @@ Sessions session.fireEvent(Ticker) Controller(components) - .render(1) + .render(1) // render takes the initial model value, in this case our model is the progress as an Int between 0 and 100. We start with 1 and increment it in the components function .iterator .takeWhile(_.model < 100) // terminate when model == 100 .foreach(_ => ()) // and run it From af8d1dcd20a9357ea5159cacad070e071164c041 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 7 Mar 2024 20:02:18 +0000 Subject: [PATCH 311/313] - --- docs/tutorial.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/docs/tutorial.md b/docs/tutorial.md index 701f0d32..8d5f0d9e 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -449,3 +449,44 @@ val output = Paragraph(text = if events.isChangedValue(emailInput) then s"Email When we update the model, we set `email = events.changedValue(emailInput, form.email)`. If the event was an `OnChange` event for our `emailInput`, this will set the email to the changed value. If not it will revert back to the `form.email`, effectively leaving the email unchanged. +## Creating reusable UI components. + +When we create user interfaces, often we want to reuse our own components. + +For instance we may want a component that asks the name of a `Person`. But we want to also be able +to add this component inside another component that is a table of `Seq[Person]` which lists all people and allows the user +to edit them. + +With terminal21, a component is just a function. It would normally take a model and `Events` but not necessarily, i.e. there can +be components that don't have to process events. Also the return +value is up to us, usually we would need to return at least a `UiElement` like `Paragraph` but many times return the updated model too. +The component that renders a page should return `MV[Model]` but the rest of the components can return what they see fit. + +Let's see the `Person` example. Here we have 2 components, `personComponent` that asks for the name of a particular `Person` and +`peopleComponent` that renders a table with a Seq[Person], using the `personComponent`. + +```scala +case class Person(id: Int, name: String) +def personComponent(person: Person, events: Events): MV[Person] = + val nameInput = Input(s"person-${person.id}", defaultValue = person.name) + val component = Box() + .withChildren( + Text(text = "Name"), + nameInput + ) + MV( + person.copy( + name = events.changedValue(nameInput, person.name) + ), + component + ) + +def peopleComponent(people: Seq[Person], events: Events): MV[Seq[Person]] = + val peopleComponents = people.map(p => personComponent(p, events)) + val component = QuickTable("people") + .withRows(peopleComponents.map(p => Seq(p.view))) + MV(peopleComponents.map(_.model), component) +``` + +`personComponent` take a `Person` model, renders an input box for the person's name and also if there is a change event for this input it updates the model accordingly. +Now `peopleComponent` creates a table and each row contains the `personComponent`. The `Seq[Person]` model is updated accordingly depending on changes propagating from `personComponent`. From 3425eeb6a0daaf94277b98ea734c05c686064c49 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 7 Mar 2024 20:05:27 +0000 Subject: [PATCH 312/313] - --- docs/tutorial.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/tutorial.md b/docs/tutorial.md index 8d5f0d9e..545283c5 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -16,7 +16,20 @@ This tutorial will use `scala-cli` but the same applies for `sbt` or `mill` proj have `scala-cli` installed on your box, you're good to go, there are no other requirements to run terminal21 scripts. Jdk and dependencies will be downloaded by `scala-cli` for us. -All example code is under `example-scripts` of this repo, feel free to checkout the repo and run them. +All example code is under `example-scripts` of this repo, feel free to checkout the repo and run them: + +```shell +git clone https://github.com/kostaskougios/terminal21-restapi.git +cd terminal21-restapi/example-scripts + +# start the server +./server.sc +# ... it will download dependencies & jdk and start the server. Point your browser to http://localhost:8080/ui/ + +# Open an other terminal window and +./hello-world.sc +# Have a look at your browser now. +``` ## Starting the terminal21 server From 6ef7e4e53b485b3b32624ca8a97498dc747e2e81 Mon Sep 17 00:00:00 2001 From: Kostas Kougios Date: Thu, 7 Mar 2024 20:18:40 +0000 Subject: [PATCH 313/313] - --- docs/tutorial.md | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/docs/tutorial.md b/docs/tutorial.md index 545283c5..3bebc76a 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -252,16 +252,7 @@ Controller(components) ``` Thats it. We have a progress bar that displays different messages depending on the stage of our universe creation. And our code would -also be easily testable. The `components` is just a function that returns the model and the UI components, so we can easily assert what -is rendered based on the model value and if the model is updated correctly based on events. A nicer way to structure each page of -our user interface would be to have it in a class with a `components` and a `controller` function. That class would be easily testable, -see the following classes if you would like to find out more. It is an app with 2 pages, a login and loggedin page: - -[LoginPage & LoggedInPage](../end-to-end-tests/src/main/scala/tests/LoginPage.scala) - -[LoginPageTest](../end-to-end-tests/src/test/scala/tests/LoginPageTest.scala) - -[LoggedInTest](../end-to-end-tests/src/test/scala/tests/LoggedInTest.scala) +also be easily testable. More on tests later on. ## Handling clicks @@ -503,3 +494,19 @@ def peopleComponent(people: Seq[Person], events: Events): MV[Seq[Person]] = `personComponent` take a `Person` model, renders an input box for the person's name and also if there is a change event for this input it updates the model accordingly. Now `peopleComponent` creates a table and each row contains the `personComponent`. The `Seq[Person]` model is updated accordingly depending on changes propagating from `personComponent`. + +## Testing + +So far we have seen that structuring our code to a `components`, `controller` and `run()` method allows us to test them easily. + +The `components` is just a function that returns the model and the UI components, so we can easily assert what +is rendered based on the model value and if the model is updated correctly based on events. Terminal21's UI components +are just case classes that can easily be compared. + +If you would like to find out more please see this 2 page app, a login and loggedin page, along with their tests: + +[LoginPage & LoggedInPage](../end-to-end-tests/src/main/scala/tests/LoginPage.scala) + +[LoginPageTest](../end-to-end-tests/src/test/scala/tests/LoginPageTest.scala) + +[LoggedInTest](../end-to-end-tests/src/test/scala/tests/LoggedInTest.scala)