From 7e605709717194a4f484a80db6308c6c94562b82 Mon Sep 17 00:00:00 2001 From: Casey Brooks Date: Fri, 1 Sep 2023 17:42:01 -0500 Subject: [PATCH] Basic storefront with datatable in Destkop example --- .../api/android/ballast-debugger-client.api | 21 +- .../api/jvm/ballast-debugger-client.api | 21 +- ballast-debugger-client/build.gradle.kts | 2 +- .../BallastDebuggerClientConnection.kt | 27 +- .../debugger/BallastDebuggerInterceptor.kt | 73 ++--- .../ballast/debugger/JsonDebuggerAdapter.kt | 58 ++++ .../ballast/debugger/LambdaDebuggerAdapter.kt | 36 +++ .../debugger/ToStringDebuggerAdapter.kt | 31 ++ .../api/android/ballast-debugger-models.api | 34 ++- .../api/jvm/ballast-debugger-models.api | 34 ++- .../BallastDebuggerViewModelConnection.kt | 7 +- .../ballast/debugger/DebuggerAdapter.kt | 25 ++ .../ballast/debugger/models/json.kt | 24 +- .../resources/wiki/usage/migration/v4.md | 24 ++ examples/desktop/build.gradle.kts | 3 + .../injector/ComposeDesktopInjector.kt | 8 + .../injector/ComposeDesktopInjectorImpl.kt | 33 +- .../com/copperleaf/ballast/examples/main.kt | 16 +- .../examples/router/BallastExamples.kt | 1 + .../router/RouterSavedStateAdapter.kt | 6 +- .../ui/datatable/DataTableContract.kt | 45 +++ .../ui/datatable/DataTableInputHandler.kt | 123 ++++++++ .../examples/ui/datatable/DataTableUi.kt | 282 ++++++++++++++++++ .../ui/datatable/DataTableViewModel.kt | 21 ++ .../ui/datatable/models/ColumnSort.kt | 10 + .../examples/ui/datatable/models/Product.kt | 14 + .../ui/datatable/models/ProductColumn.kt | 11 + .../ui/datatable/models/SortDirection.kt | 6 + .../ui/datatable/utils/generateProducts.kt | 42 +++ .../examples/ui/datatable/utils/query.kt | 100 +++++++ .../injector/ComposeWebInjectorImpl.kt | 2 +- gradle-convention-plugins | 2 +- settings.gradle.kts | 2 +- 33 files changed, 1017 insertions(+), 127 deletions(-) create mode 100644 ballast-debugger-client/src/commonMain/kotlin/com/copperleaf/ballast/debugger/JsonDebuggerAdapter.kt create mode 100644 ballast-debugger-client/src/commonMain/kotlin/com/copperleaf/ballast/debugger/LambdaDebuggerAdapter.kt create mode 100644 ballast-debugger-client/src/commonMain/kotlin/com/copperleaf/ballast/debugger/ToStringDebuggerAdapter.kt create mode 100644 ballast-debugger-models/src/commonMain/kotlin/com/copperleaf/ballast/debugger/DebuggerAdapter.kt create mode 100644 examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/DataTableContract.kt create mode 100644 examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/DataTableInputHandler.kt create mode 100644 examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/DataTableUi.kt create mode 100644 examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/DataTableViewModel.kt create mode 100644 examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/models/ColumnSort.kt create mode 100644 examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/models/Product.kt create mode 100644 examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/models/ProductColumn.kt create mode 100644 examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/models/SortDirection.kt create mode 100644 examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/utils/generateProducts.kt create mode 100644 examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/utils/query.kt diff --git a/ballast-debugger-client/api/android/ballast-debugger-client.api b/ballast-debugger-client/api/android/ballast-debugger-client.api index 81088d2d..38078308 100644 --- a/ballast-debugger-client/api/android/ballast-debugger-client.api +++ b/ballast-debugger-client/api/android/ballast-debugger-client.api @@ -11,15 +11,28 @@ public final class com/copperleaf/ballast/debugger/BallastDebuggerClientConnecti public final class com/copperleaf/ballast/debugger/BallastDebuggerInterceptor : com/copperleaf/ballast/BallastInterceptor { public static final field Companion Lcom/copperleaf/ballast/debugger/BallastDebuggerInterceptor$Companion; - public fun (Lcom/copperleaf/ballast/debugger/BallastDebuggerClientConnection;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)V - public synthetic fun (Lcom/copperleaf/ballast/debugger/BallastDebuggerClientConnection;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lcom/copperleaf/ballast/debugger/BallastDebuggerClientConnection;Lcom/copperleaf/ballast/debugger/DebuggerAdapter;)V + public synthetic fun (Lcom/copperleaf/ballast/debugger/BallastDebuggerClientConnection;Lcom/copperleaf/ballast/debugger/DebuggerAdapter;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun getKey ()Lcom/copperleaf/ballast/BallastInterceptor$Key; public fun start (Lcom/copperleaf/ballast/BallastInterceptorScope;Lkotlinx/coroutines/flow/Flow;)V public fun toString ()Ljava/lang/String; } public final class com/copperleaf/ballast/debugger/BallastDebuggerInterceptor$Companion { - public final fun withJson (Lcom/copperleaf/ballast/debugger/BallastDebuggerClientConnection;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/KSerializer;)Lcom/copperleaf/ballast/debugger/BallastDebuggerInterceptor; - public static synthetic fun withJson$default (Lcom/copperleaf/ballast/debugger/BallastDebuggerInterceptor$Companion;Lcom/copperleaf/ballast/debugger/BallastDebuggerClientConnection;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/KSerializer;ILjava/lang/Object;)Lcom/copperleaf/ballast/debugger/BallastDebuggerInterceptor; + public final fun invoke (Lcom/copperleaf/ballast/debugger/BallastDebuggerClientConnection;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lcom/copperleaf/ballast/debugger/BallastDebuggerInterceptor; + public final fun invoke (Lcom/copperleaf/ballast/debugger/BallastDebuggerClientConnection;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/json/Json;)Lcom/copperleaf/ballast/debugger/BallastDebuggerInterceptor; + public static synthetic fun invoke$default (Lcom/copperleaf/ballast/debugger/BallastDebuggerInterceptor$Companion;Lcom/copperleaf/ballast/debugger/BallastDebuggerClientConnection;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/json/Json;ILjava/lang/Object;)Lcom/copperleaf/ballast/debugger/BallastDebuggerInterceptor; +} + +public final class com/copperleaf/ballast/debugger/JsonDebuggerAdapter : com/copperleaf/ballast/debugger/DebuggerAdapter { + public fun ()V + public fun (Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/json/Json;)V + public synthetic fun (Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/json/Json;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun deserializeInput (Lio/ktor/http/ContentType;Ljava/lang/String;)Ljava/lang/Object; + public fun deserializeState (Lio/ktor/http/ContentType;Ljava/lang/String;)Ljava/lang/Object; + public fun serializeEvent (Ljava/lang/Object;)Lkotlin/Pair; + public fun serializeInput (Ljava/lang/Object;)Lkotlin/Pair; + public fun serializeState (Ljava/lang/Object;)Lkotlin/Pair; + public fun toString ()Ljava/lang/String; } diff --git a/ballast-debugger-client/api/jvm/ballast-debugger-client.api b/ballast-debugger-client/api/jvm/ballast-debugger-client.api index 81088d2d..38078308 100644 --- a/ballast-debugger-client/api/jvm/ballast-debugger-client.api +++ b/ballast-debugger-client/api/jvm/ballast-debugger-client.api @@ -11,15 +11,28 @@ public final class com/copperleaf/ballast/debugger/BallastDebuggerClientConnecti public final class com/copperleaf/ballast/debugger/BallastDebuggerInterceptor : com/copperleaf/ballast/BallastInterceptor { public static final field Companion Lcom/copperleaf/ballast/debugger/BallastDebuggerInterceptor$Companion; - public fun (Lcom/copperleaf/ballast/debugger/BallastDebuggerClientConnection;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)V - public synthetic fun (Lcom/copperleaf/ballast/debugger/BallastDebuggerClientConnection;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lcom/copperleaf/ballast/debugger/BallastDebuggerClientConnection;Lcom/copperleaf/ballast/debugger/DebuggerAdapter;)V + public synthetic fun (Lcom/copperleaf/ballast/debugger/BallastDebuggerClientConnection;Lcom/copperleaf/ballast/debugger/DebuggerAdapter;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun getKey ()Lcom/copperleaf/ballast/BallastInterceptor$Key; public fun start (Lcom/copperleaf/ballast/BallastInterceptorScope;Lkotlinx/coroutines/flow/Flow;)V public fun toString ()Ljava/lang/String; } public final class com/copperleaf/ballast/debugger/BallastDebuggerInterceptor$Companion { - public final fun withJson (Lcom/copperleaf/ballast/debugger/BallastDebuggerClientConnection;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/KSerializer;)Lcom/copperleaf/ballast/debugger/BallastDebuggerInterceptor; - public static synthetic fun withJson$default (Lcom/copperleaf/ballast/debugger/BallastDebuggerInterceptor$Companion;Lcom/copperleaf/ballast/debugger/BallastDebuggerClientConnection;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/KSerializer;ILjava/lang/Object;)Lcom/copperleaf/ballast/debugger/BallastDebuggerInterceptor; + public final fun invoke (Lcom/copperleaf/ballast/debugger/BallastDebuggerClientConnection;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lcom/copperleaf/ballast/debugger/BallastDebuggerInterceptor; + public final fun invoke (Lcom/copperleaf/ballast/debugger/BallastDebuggerClientConnection;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/json/Json;)Lcom/copperleaf/ballast/debugger/BallastDebuggerInterceptor; + public static synthetic fun invoke$default (Lcom/copperleaf/ballast/debugger/BallastDebuggerInterceptor$Companion;Lcom/copperleaf/ballast/debugger/BallastDebuggerClientConnection;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/json/Json;ILjava/lang/Object;)Lcom/copperleaf/ballast/debugger/BallastDebuggerInterceptor; +} + +public final class com/copperleaf/ballast/debugger/JsonDebuggerAdapter : com/copperleaf/ballast/debugger/DebuggerAdapter { + public fun ()V + public fun (Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/json/Json;)V + public synthetic fun (Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/json/Json;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun deserializeInput (Lio/ktor/http/ContentType;Ljava/lang/String;)Ljava/lang/Object; + public fun deserializeState (Lio/ktor/http/ContentType;Ljava/lang/String;)Ljava/lang/Object; + public fun serializeEvent (Ljava/lang/Object;)Lkotlin/Pair; + public fun serializeInput (Ljava/lang/Object;)Lkotlin/Pair; + public fun serializeState (Ljava/lang/Object;)Lkotlin/Pair; + public fun toString ()Ljava/lang/String; } diff --git a/ballast-debugger-client/build.gradle.kts b/ballast-debugger-client/build.gradle.kts index 2419736e..fabb179e 100644 --- a/ballast-debugger-client/build.gradle.kts +++ b/ballast-debugger-client/build.gradle.kts @@ -16,7 +16,7 @@ kotlin { val commonMain by getting { dependencies { implementation(project(":ballast-api")) - implementation(project(":ballast-debugger-models")) + api(project(":ballast-debugger-models")) implementation(libs.bundles.ktorClient) implementation(libs.kotlinx.datetime) diff --git a/ballast-debugger-client/src/commonMain/kotlin/com/copperleaf/ballast/debugger/BallastDebuggerClientConnection.kt b/ballast-debugger-client/src/commonMain/kotlin/com/copperleaf/ballast/debugger/BallastDebuggerClientConnection.kt index d81ac571..132d0312 100644 --- a/ballast-debugger-client/src/commonMain/kotlin/com/copperleaf/ballast/debugger/BallastDebuggerClientConnection.kt +++ b/ballast-debugger-client/src/commonMain/kotlin/com/copperleaf/ballast/debugger/BallastDebuggerClientConnection.kt @@ -388,13 +388,8 @@ public class BallastDebuggerClientConnection( } is BallastDebuggerActionV4.RequestReplaceState -> { - if (viewModelConnection.deserializeState == null) { - logger.info("States cannot be replaced from serialized state.") - return - } - val stateToReplaceResult = runCatching { - viewModelConnection.deserializeState!!( + viewModelConnection.adapter.deserializeState( ContentType.parse(action.stateContentType), action.serializedState, ) @@ -402,7 +397,11 @@ public class BallastDebuggerClientConnection( stateToReplaceResult.fold( onSuccess = { state -> - sendToQueue(Queued.RestoreState(null, state)) + if (state != null) { + sendToQueue(Queued.RestoreState(null, state)) + } else { + logger.info("This ViewModel is not configured to replace States from serialized state.") + } }, onFailure = { error -> logger.info("Serialized state is formatted incorrectly") @@ -420,17 +419,13 @@ public class BallastDebuggerClientConnection( if (inputToResend != null) { sendToQueue(Queued.HandleInput(null, inputToResend)) } else { + logger.info("Could not find requested Input to resend.") } } is BallastDebuggerActionV4.RequestSendInput -> { - if (viewModelConnection.deserializeInput == null) { - logger.info("Inputs cannot be replaced from serialized state.") - return - } - val inputToSendResult = runCatching { - viewModelConnection.deserializeInput!!( + viewModelConnection.adapter.deserializeInput( ContentType.parse(action.inputContentType), action.serializedInput, ) @@ -438,7 +433,11 @@ public class BallastDebuggerClientConnection( inputToSendResult.fold( onSuccess = { input -> - sendToQueue(Queued.HandleInput(null, input)) + if (input != null) { + sendToQueue(Queued.HandleInput(null, input)) + } else { + logger.info("This ViewModel is not configured to send serialized Inputs.") + } }, onFailure = { error -> logger.info("Serialized input is formatted incorrectly") diff --git a/ballast-debugger-client/src/commonMain/kotlin/com/copperleaf/ballast/debugger/BallastDebuggerInterceptor.kt b/ballast-debugger-client/src/commonMain/kotlin/com/copperleaf/ballast/debugger/BallastDebuggerInterceptor.kt index d1c33f30..d81dc206 100644 --- a/ballast-debugger-client/src/commonMain/kotlin/com/copperleaf/ballast/debugger/BallastDebuggerInterceptor.kt +++ b/ballast-debugger-client/src/commonMain/kotlin/com/copperleaf/ballast/debugger/BallastDebuggerInterceptor.kt @@ -10,11 +10,7 @@ import kotlinx.serialization.json.Json public class BallastDebuggerInterceptor( private val connection: BallastDebuggerClientConnection<*>, - private val serializeInput: (Inputs) -> Pair = { ContentType.Text.Any to it.toString() }, - private val serializeEvent: (Events) -> Pair = { ContentType.Text.Any to it.toString() }, - private val serializeState: (State) -> Pair = { ContentType.Text.Any to it.toString() }, - private val deserializeState: ((ContentType, String) -> State)? = null, - private val deserializeInput: ((ContentType, String) -> Inputs)? = null, + private val adapter: DebuggerAdapter = ToStringDebuggerAdapter(), ) : BallastInterceptor { override fun BallastInterceptorScope.start(notifications: Flow>) { @@ -23,11 +19,7 @@ public class BallastDebuggerInterceptor BallastDebuggerViewModelConnection( notifications = notifications, viewModelName = hostViewModelName, - serializeInput = serializeInput, - serializeEvent = serializeEvent, - serializeState = serializeState, - deserializeState = deserializeState, - deserializeInput = deserializeInput, + adapter = adapter, ) ) } @@ -38,47 +30,38 @@ public class BallastDebuggerInterceptor } public companion object { - public fun withJson( + + public operator fun invoke( + connection: BallastDebuggerClientConnection<*>, + serializeInput: (Inputs) -> Pair, + serializeEvent: (Events) -> Pair, + serializeState: (State) -> Pair, + ): BallastDebuggerInterceptor { + return BallastDebuggerInterceptor( + connection, + LambdaDebuggerAdapter( + serializeInput = serializeInput, + serializeEvent = serializeEvent, + serializeState = serializeState, + ) + ) + } + + public operator fun invoke( connection: BallastDebuggerClientConnection<*>, - stateSerializer: KSerializer? = null, inputsSerializer: KSerializer? = null, eventsSerializer: KSerializer? = null, + stateSerializer: KSerializer? = null, + json: Json = Json, ): BallastDebuggerInterceptor { - val json = ContentType.Application.Json - val plainText = ContentType.Text.Any return BallastDebuggerInterceptor( connection, - serializeInput = if (inputsSerializer != null) { - { input -> json to Json.encodeToString(inputsSerializer, input) } - } else { - { input -> plainText to input.toString() } - }, - serializeEvent = if (eventsSerializer != null) { - { event -> json to Json.encodeToString(eventsSerializer, event) } - } else { - { event -> plainText to event.toString() } - }, - serializeState = if (stateSerializer != null) { - { state -> json to Json.encodeToString(stateSerializer, state) } - } else { - { state -> plainText to state.toString() } - }, - deserializeInput = if (inputsSerializer != null) { - { contentType: ContentType, serializedInput: String -> - check(contentType == json) - Json.decodeFromString(inputsSerializer, serializedInput) - } - } else { - null - }, - deserializeState = if (stateSerializer != null) { - { contentType: ContentType, serializedState: String -> - check(contentType == json) - Json.decodeFromString(stateSerializer, serializedState) - } - } else { - null - }, + JsonDebuggerAdapter( + inputsSerializer = inputsSerializer, + eventsSerializer = eventsSerializer, + stateSerializer = stateSerializer, + json = json, + ) ) } } diff --git a/ballast-debugger-client/src/commonMain/kotlin/com/copperleaf/ballast/debugger/JsonDebuggerAdapter.kt b/ballast-debugger-client/src/commonMain/kotlin/com/copperleaf/ballast/debugger/JsonDebuggerAdapter.kt new file mode 100644 index 00000000..0ebc7640 --- /dev/null +++ b/ballast-debugger-client/src/commonMain/kotlin/com/copperleaf/ballast/debugger/JsonDebuggerAdapter.kt @@ -0,0 +1,58 @@ +package com.copperleaf.ballast.debugger + +import io.ktor.http.ContentType +import kotlinx.serialization.KSerializer +import kotlinx.serialization.json.Json + +public class JsonDebuggerAdapter( + private val inputsSerializer: KSerializer? = null, + private val eventsSerializer: KSerializer? = null, + private val stateSerializer: KSerializer? = null, + private val json: Json = Json, +) : DebuggerAdapter { + override fun serializeInput(input: Inputs): Pair { + return if (inputsSerializer != null) { + ContentType.Application.Json to json.encodeToString(inputsSerializer, input) + } else { + ContentType.Text.Any to input.toString() + } + } + + override fun serializeEvent(event: Events): Pair { + return if (eventsSerializer != null) { + ContentType.Application.Json to json.encodeToString(eventsSerializer, event) + } else { + ContentType.Text.Any to event.toString() + } + } + + override fun serializeState(state: State): Pair { + return if (stateSerializer != null) { + ContentType.Application.Json to json.encodeToString(stateSerializer, state) + } else { + ContentType.Text.Any to state.toString() + } + } + + override fun deserializeInput(contentType: ContentType, serializedInput: String): Inputs? { + return if (inputsSerializer != null) { + check(contentType == ContentType.Application.Json) + json.decodeFromString(inputsSerializer, serializedInput) + } else { + null + } + } + + override fun deserializeState(contentType: ContentType, serializedState: String): State? { + return if (stateSerializer != null) { + check(contentType == ContentType.Application.Json) + json.decodeFromString(stateSerializer, serializedState) + } else { + null + } + } + + override fun toString(): String { + return "JsonDebuggerAdapter" + } +} diff --git a/ballast-debugger-client/src/commonMain/kotlin/com/copperleaf/ballast/debugger/LambdaDebuggerAdapter.kt b/ballast-debugger-client/src/commonMain/kotlin/com/copperleaf/ballast/debugger/LambdaDebuggerAdapter.kt new file mode 100644 index 00000000..0055ffc2 --- /dev/null +++ b/ballast-debugger-client/src/commonMain/kotlin/com/copperleaf/ballast/debugger/LambdaDebuggerAdapter.kt @@ -0,0 +1,36 @@ +package com.copperleaf.ballast.debugger + +import io.ktor.http.ContentType + +internal class LambdaDebuggerAdapter( + private val serializeInput: ((Inputs) -> Pair)?, + private val serializeEvent: ((Events) -> Pair)?, + private val serializeState: ((State) -> Pair)?, +) : DebuggerAdapter { + override fun serializeInput(input: Inputs): Pair { + return serializeInput?.invoke(input) + ?: (ContentType.Text.Any to input.toString()) + } + + override fun serializeEvent(event: Events): Pair { + return serializeEvent?.invoke(event) + ?: (ContentType.Text.Any to event.toString()) + } + + override fun serializeState(state: State): Pair { + return serializeState?.invoke(state) + ?: (ContentType.Text.Any to state.toString()) + } + + override fun deserializeInput(contentType: ContentType, serializedInput: String): Inputs? { + return null + } + + override fun deserializeState(contentType: ContentType, serializedState: String): State? { + return null + } + + override fun toString(): String { + return "LambdaDebuggerAdapter" + } +} diff --git a/ballast-debugger-client/src/commonMain/kotlin/com/copperleaf/ballast/debugger/ToStringDebuggerAdapter.kt b/ballast-debugger-client/src/commonMain/kotlin/com/copperleaf/ballast/debugger/ToStringDebuggerAdapter.kt new file mode 100644 index 00000000..fd720695 --- /dev/null +++ b/ballast-debugger-client/src/commonMain/kotlin/com/copperleaf/ballast/debugger/ToStringDebuggerAdapter.kt @@ -0,0 +1,31 @@ +package com.copperleaf.ballast.debugger + +import io.ktor.http.ContentType + +public class ToStringDebuggerAdapter : + DebuggerAdapter { + + override fun serializeInput(input: Inputs): Pair { + return ContentType.Text.Any to input.toString() + } + + override fun serializeEvent(event: Events): Pair { + return ContentType.Text.Any to event.toString() + } + + override fun serializeState(state: State): Pair { + return ContentType.Text.Any to state.toString() + } + + override fun deserializeInput(contentType: ContentType, serializedInput: String): Inputs? { + return null + } + + override fun deserializeState(contentType: ContentType, serializedState: String): State? { + return null + } + + override fun toString(): String { + return "ToStringDebuggerAdapter" + } +} diff --git a/ballast-debugger-models/api/android/ballast-debugger-models.api b/ballast-debugger-models/api/android/ballast-debugger-models.api index daa8636e..041a76b6 100644 --- a/ballast-debugger-models/api/android/ballast-debugger-models.api +++ b/ballast-debugger-models/api/android/ballast-debugger-models.api @@ -8,23 +8,15 @@ public final class com/copperleaf/ballast/debugger/BallastDebuggerOutgoingEventW } public final class com/copperleaf/ballast/debugger/BallastDebuggerViewModelConnection { - public fun (Ljava/lang/String;Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)V + public fun (Ljava/lang/String;Lkotlinx/coroutines/flow/Flow;Lcom/copperleaf/ballast/debugger/DebuggerAdapter;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()Lkotlinx/coroutines/flow/Flow; - public final fun component3 ()Lkotlin/jvm/functions/Function1; - public final fun component4 ()Lkotlin/jvm/functions/Function1; - public final fun component5 ()Lkotlin/jvm/functions/Function1; - public final fun component6 ()Lkotlin/jvm/functions/Function2; - public final fun component7 ()Lkotlin/jvm/functions/Function2; - public final fun copy (Ljava/lang/String;Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)Lcom/copperleaf/ballast/debugger/BallastDebuggerViewModelConnection; - public static synthetic fun copy$default (Lcom/copperleaf/ballast/debugger/BallastDebuggerViewModelConnection;Ljava/lang/String;Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/copperleaf/ballast/debugger/BallastDebuggerViewModelConnection; + public final fun component3 ()Lcom/copperleaf/ballast/debugger/DebuggerAdapter; + public final fun copy (Ljava/lang/String;Lkotlinx/coroutines/flow/Flow;Lcom/copperleaf/ballast/debugger/DebuggerAdapter;)Lcom/copperleaf/ballast/debugger/BallastDebuggerViewModelConnection; + public static synthetic fun copy$default (Lcom/copperleaf/ballast/debugger/BallastDebuggerViewModelConnection;Ljava/lang/String;Lkotlinx/coroutines/flow/Flow;Lcom/copperleaf/ballast/debugger/DebuggerAdapter;ILjava/lang/Object;)Lcom/copperleaf/ballast/debugger/BallastDebuggerViewModelConnection; public fun equals (Ljava/lang/Object;)Z - public final fun getDeserializeInput ()Lkotlin/jvm/functions/Function2; - public final fun getDeserializeState ()Lkotlin/jvm/functions/Function2; + public final fun getAdapter ()Lcom/copperleaf/ballast/debugger/DebuggerAdapter; public final fun getNotifications ()Lkotlinx/coroutines/flow/Flow; - public final fun getSerializeEvent ()Lkotlin/jvm/functions/Function1; - public final fun getSerializeInput ()Lkotlin/jvm/functions/Function1; - public final fun getSerializeState ()Lkotlin/jvm/functions/Function1; public final fun getViewModelName ()Ljava/lang/String; public fun hashCode ()I public fun toString ()Ljava/lang/String; @@ -35,6 +27,22 @@ public final class com/copperleaf/ballast/debugger/BallastDebuggerViewModelConne public static final field CONNECTION_ID_HEADER Ljava/lang/String; } +public abstract interface class com/copperleaf/ballast/debugger/DebuggerAdapter { + public abstract fun deserializeInput (Lio/ktor/http/ContentType;Ljava/lang/String;)Ljava/lang/Object; + public abstract fun deserializeState (Lio/ktor/http/ContentType;Ljava/lang/String;)Ljava/lang/Object; + public abstract fun serializeEvent (Ljava/lang/Object;)Lkotlin/Pair; + public abstract fun serializeInput (Ljava/lang/Object;)Lkotlin/Pair; + public abstract fun serializeState (Ljava/lang/Object;)Lkotlin/Pair; +} + +public final class com/copperleaf/ballast/debugger/DebuggerAdapter$DefaultImpls { + public static fun deserializeInput (Lcom/copperleaf/ballast/debugger/DebuggerAdapter;Lio/ktor/http/ContentType;Ljava/lang/String;)Ljava/lang/Object; + public static fun deserializeState (Lcom/copperleaf/ballast/debugger/DebuggerAdapter;Lio/ktor/http/ContentType;Ljava/lang/String;)Ljava/lang/Object; + public static fun serializeEvent (Lcom/copperleaf/ballast/debugger/DebuggerAdapter;Ljava/lang/Object;)Lkotlin/Pair; + public static fun serializeInput (Lcom/copperleaf/ballast/debugger/DebuggerAdapter;Ljava/lang/Object;)Lkotlin/Pair; + public static fun serializeState (Lcom/copperleaf/ballast/debugger/DebuggerAdapter;Ljava/lang/Object;)Lkotlin/Pair; +} + public final class com/copperleaf/ballast/debugger/models/BallastApplicationState { public fun ()V public fun (Ljava/util/List;)V diff --git a/ballast-debugger-models/api/jvm/ballast-debugger-models.api b/ballast-debugger-models/api/jvm/ballast-debugger-models.api index daa8636e..041a76b6 100644 --- a/ballast-debugger-models/api/jvm/ballast-debugger-models.api +++ b/ballast-debugger-models/api/jvm/ballast-debugger-models.api @@ -8,23 +8,15 @@ public final class com/copperleaf/ballast/debugger/BallastDebuggerOutgoingEventW } public final class com/copperleaf/ballast/debugger/BallastDebuggerViewModelConnection { - public fun (Ljava/lang/String;Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)V + public fun (Ljava/lang/String;Lkotlinx/coroutines/flow/Flow;Lcom/copperleaf/ballast/debugger/DebuggerAdapter;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()Lkotlinx/coroutines/flow/Flow; - public final fun component3 ()Lkotlin/jvm/functions/Function1; - public final fun component4 ()Lkotlin/jvm/functions/Function1; - public final fun component5 ()Lkotlin/jvm/functions/Function1; - public final fun component6 ()Lkotlin/jvm/functions/Function2; - public final fun component7 ()Lkotlin/jvm/functions/Function2; - public final fun copy (Ljava/lang/String;Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)Lcom/copperleaf/ballast/debugger/BallastDebuggerViewModelConnection; - public static synthetic fun copy$default (Lcom/copperleaf/ballast/debugger/BallastDebuggerViewModelConnection;Ljava/lang/String;Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/copperleaf/ballast/debugger/BallastDebuggerViewModelConnection; + public final fun component3 ()Lcom/copperleaf/ballast/debugger/DebuggerAdapter; + public final fun copy (Ljava/lang/String;Lkotlinx/coroutines/flow/Flow;Lcom/copperleaf/ballast/debugger/DebuggerAdapter;)Lcom/copperleaf/ballast/debugger/BallastDebuggerViewModelConnection; + public static synthetic fun copy$default (Lcom/copperleaf/ballast/debugger/BallastDebuggerViewModelConnection;Ljava/lang/String;Lkotlinx/coroutines/flow/Flow;Lcom/copperleaf/ballast/debugger/DebuggerAdapter;ILjava/lang/Object;)Lcom/copperleaf/ballast/debugger/BallastDebuggerViewModelConnection; public fun equals (Ljava/lang/Object;)Z - public final fun getDeserializeInput ()Lkotlin/jvm/functions/Function2; - public final fun getDeserializeState ()Lkotlin/jvm/functions/Function2; + public final fun getAdapter ()Lcom/copperleaf/ballast/debugger/DebuggerAdapter; public final fun getNotifications ()Lkotlinx/coroutines/flow/Flow; - public final fun getSerializeEvent ()Lkotlin/jvm/functions/Function1; - public final fun getSerializeInput ()Lkotlin/jvm/functions/Function1; - public final fun getSerializeState ()Lkotlin/jvm/functions/Function1; public final fun getViewModelName ()Ljava/lang/String; public fun hashCode ()I public fun toString ()Ljava/lang/String; @@ -35,6 +27,22 @@ public final class com/copperleaf/ballast/debugger/BallastDebuggerViewModelConne public static final field CONNECTION_ID_HEADER Ljava/lang/String; } +public abstract interface class com/copperleaf/ballast/debugger/DebuggerAdapter { + public abstract fun deserializeInput (Lio/ktor/http/ContentType;Ljava/lang/String;)Ljava/lang/Object; + public abstract fun deserializeState (Lio/ktor/http/ContentType;Ljava/lang/String;)Ljava/lang/Object; + public abstract fun serializeEvent (Ljava/lang/Object;)Lkotlin/Pair; + public abstract fun serializeInput (Ljava/lang/Object;)Lkotlin/Pair; + public abstract fun serializeState (Ljava/lang/Object;)Lkotlin/Pair; +} + +public final class com/copperleaf/ballast/debugger/DebuggerAdapter$DefaultImpls { + public static fun deserializeInput (Lcom/copperleaf/ballast/debugger/DebuggerAdapter;Lio/ktor/http/ContentType;Ljava/lang/String;)Ljava/lang/Object; + public static fun deserializeState (Lcom/copperleaf/ballast/debugger/DebuggerAdapter;Lio/ktor/http/ContentType;Ljava/lang/String;)Ljava/lang/Object; + public static fun serializeEvent (Lcom/copperleaf/ballast/debugger/DebuggerAdapter;Ljava/lang/Object;)Lkotlin/Pair; + public static fun serializeInput (Lcom/copperleaf/ballast/debugger/DebuggerAdapter;Ljava/lang/Object;)Lkotlin/Pair; + public static fun serializeState (Lcom/copperleaf/ballast/debugger/DebuggerAdapter;Ljava/lang/Object;)Lkotlin/Pair; +} + public final class com/copperleaf/ballast/debugger/models/BallastApplicationState { public fun ()V public fun (Ljava/util/List;)V diff --git a/ballast-debugger-models/src/commonMain/kotlin/com/copperleaf/ballast/debugger/BallastDebuggerViewModelConnection.kt b/ballast-debugger-models/src/commonMain/kotlin/com/copperleaf/ballast/debugger/BallastDebuggerViewModelConnection.kt index 7604090a..7bdfd656 100644 --- a/ballast-debugger-models/src/commonMain/kotlin/com/copperleaf/ballast/debugger/BallastDebuggerViewModelConnection.kt +++ b/ballast-debugger-models/src/commonMain/kotlin/com/copperleaf/ballast/debugger/BallastDebuggerViewModelConnection.kt @@ -3,7 +3,6 @@ package com.copperleaf.ballast.debugger import com.copperleaf.ballast.BallastNotification import com.copperleaf.ballast.debugger.models.serialize import com.copperleaf.ballast.debugger.versions.v4.BallastDebuggerEventV4 -import io.ktor.http.ContentType import kotlinx.coroutines.flow.Flow import kotlinx.datetime.LocalDateTime @@ -13,11 +12,7 @@ public const val BALLAST_VERSION_HEADER: String = "x-ballast-version" public data class BallastDebuggerViewModelConnection( public val viewModelName: String, public val notifications: Flow>, - public val serializeInput: (Inputs) -> Pair, - public val serializeEvent: (Events) -> Pair, - public val serializeState: (State) -> Pair, - public val deserializeState: ((ContentType, String) -> State)?, - public val deserializeInput: ((ContentType, String) -> Inputs)?, + public val adapter: DebuggerAdapter ) public class BallastDebuggerOutgoingEventWrapper( diff --git a/ballast-debugger-models/src/commonMain/kotlin/com/copperleaf/ballast/debugger/DebuggerAdapter.kt b/ballast-debugger-models/src/commonMain/kotlin/com/copperleaf/ballast/debugger/DebuggerAdapter.kt new file mode 100644 index 00000000..286a562c --- /dev/null +++ b/ballast-debugger-models/src/commonMain/kotlin/com/copperleaf/ballast/debugger/DebuggerAdapter.kt @@ -0,0 +1,25 @@ +package com.copperleaf.ballast.debugger + +import io.ktor.http.ContentType + +public interface DebuggerAdapter { + public fun serializeInput(input: Inputs): Pair { + return ContentType.Text.Any to input.toString() + } + + public fun serializeEvent(event: Events): Pair { + return ContentType.Text.Any to event.toString() + } + + public fun serializeState(state: State): Pair { + return ContentType.Text.Any to state.toString() + } + + public fun deserializeState(contentType: ContentType, serializedState: String): State? { + return null + } + + public fun deserializeInput(contentType: ContentType, serializedInput: String): Inputs? { + return null + } +} diff --git a/ballast-debugger-models/src/commonMain/kotlin/com/copperleaf/ballast/debugger/models/json.kt b/ballast-debugger-models/src/commonMain/kotlin/com/copperleaf/ballast/debugger/models/json.kt index c1fcbec2..7991b5ef 100644 --- a/ballast-debugger-models/src/commonMain/kotlin/com/copperleaf/ballast/debugger/models/json.kt +++ b/ballast-debugger-models/src/commonMain/kotlin/com/copperleaf/ballast/debugger/models/json.kt @@ -24,50 +24,50 @@ internal fun BallastNotification { - val (contentType, serializedContent) = viewModelConnection.serializeInput(input) + val (contentType, serializedContent) = viewModelConnection.adapter.serializeInput(input) BallastDebuggerEventV4.InputQueued(connectionId, viewModelName, uuid, firstSeen, input.type, serializedContent, contentType.asContentTypeString()) } is BallastNotification.InputAccepted -> { - val (contentType, serializedContent) = viewModelConnection.serializeInput(input) + val (contentType, serializedContent) = viewModelConnection.adapter.serializeInput(input) BallastDebuggerEventV4.InputAccepted(connectionId, viewModelName, uuid, now, input.type, serializedContent, contentType.asContentTypeString()) } is BallastNotification.InputRejected -> { - val (contentType, serializedContent) = viewModelConnection.serializeInput(input) + val (contentType, serializedContent) = viewModelConnection.adapter.serializeInput(input) BallastDebuggerEventV4.InputRejected(connectionId, viewModelName, uuid, now, input.type, serializedContent, contentType.asContentTypeString()) } is BallastNotification.InputDropped -> { - val (contentType, serializedContent) = viewModelConnection.serializeInput(input) + val (contentType, serializedContent) = viewModelConnection.adapter.serializeInput(input) BallastDebuggerEventV4.InputDropped(connectionId, viewModelName, uuid, now, input.type, serializedContent, contentType.asContentTypeString()) } is BallastNotification.InputHandledSuccessfully -> { - val (contentType, serializedContent) = viewModelConnection.serializeInput(input) + val (contentType, serializedContent) = viewModelConnection.adapter.serializeInput(input) BallastDebuggerEventV4.InputHandledSuccessfully(connectionId, viewModelName, uuid, now, input.type, serializedContent, contentType.asContentTypeString()) } is BallastNotification.InputCancelled -> { - val (contentType, serializedContent) = viewModelConnection.serializeInput(input) + val (contentType, serializedContent) = viewModelConnection.adapter.serializeInput(input) BallastDebuggerEventV4.InputCancelled(connectionId, viewModelName, uuid, now, input.type, serializedContent, contentType.asContentTypeString()) } is BallastNotification.InputHandlerError -> { - val (contentType, serializedContent) = viewModelConnection.serializeInput(input) + val (contentType, serializedContent) = viewModelConnection.adapter.serializeInput(input) BallastDebuggerEventV4.InputHandlerError( connectionId, viewModelName, uuid, now, input.type, serializedContent, contentType.asContentTypeString(), throwable.stackTraceToString() ) } is BallastNotification.EventQueued -> { - val (contentType, serializedContent) = viewModelConnection.serializeEvent(event) + val (contentType, serializedContent) = viewModelConnection.adapter.serializeEvent(event) BallastDebuggerEventV4.EventQueued(connectionId, viewModelName, uuid, firstSeen, event.type, serializedContent, contentType.asContentTypeString()) } is BallastNotification.EventEmitted -> { - val (contentType, serializedContent) = viewModelConnection.serializeEvent(event) + val (contentType, serializedContent) = viewModelConnection.adapter.serializeEvent(event) BallastDebuggerEventV4.EventEmitted(connectionId, viewModelName, uuid, now, event.type, serializedContent, contentType.asContentTypeString()) } is BallastNotification.EventHandledSuccessfully -> { - val (contentType, serializedContent) = viewModelConnection.serializeEvent(event) + val (contentType, serializedContent) = viewModelConnection.adapter.serializeEvent(event) BallastDebuggerEventV4.EventHandledSuccessfully(connectionId, viewModelName, uuid, now, event.type, serializedContent, contentType.asContentTypeString()) } is BallastNotification.EventHandlerError -> { - val (contentType, serializedContent) = viewModelConnection.serializeEvent(event) + val (contentType, serializedContent) = viewModelConnection.adapter.serializeEvent(event) BallastDebuggerEventV4.EventHandlerError( connectionId, viewModelName, uuid, now, event.type, serializedContent, contentType.asContentTypeString(), throwable.stackTraceToString() @@ -80,7 +80,7 @@ internal fun BallastNotification { - val (contentType, serializedContent) = viewModelConnection.serializeState(state) + val (contentType, serializedContent) = viewModelConnection.adapter.serializeState(state) BallastDebuggerEventV4.StateChanged(connectionId, viewModelName, uuid, firstSeen, state.type, serializedContent, contentType.asContentTypeString()) } diff --git a/docs/src/orchid/resources/wiki/usage/migration/v4.md b/docs/src/orchid/resources/wiki/usage/migration/v4.md index 33580611..9c6c7a1f 100644 --- a/docs/src/orchid/resources/wiki/usage/migration/v4.md +++ b/docs/src/orchid/resources/wiki/usage/migration/v4.md @@ -4,3 +4,27 @@ # Migrate from V3 to V4 Ballast v4.0.0 is a major release, which some breaking changes in its public API and many changes to its internals. + +- Removed everything deprecated since v3.0.0 +- getInitialState added to BallastInterceptorScope +- Began refactoring internals for greater flexibility. This work is only partly done, but some new features are now + available for advanced usage: + - EventStrategies + - InputStrategies no longer require buffering Inputs through a Channel. You may create your own InputStrategy for + special use-cases like immediate dispatch, or buffering though another mechanism (AWS SQS queues for example) + - InputFilter is now a property of relevant InputStrategies, rather than being a member of the Builder. +- `Builder.withViewModel()` now returns a strongly-typed variant of the Builder class, allowing for greater type-safety + - when applying Interceptors +- `ballast-debugger` artifact has been renamed to `ballast-debugger-client` +- Removed `BallastException`. It was only ever used as a base-class for other Exception types +- New Navigation Inputs added to `:ballast-naviagation` + - `RouterContract.Inputs.PopAllWithAnnotation` + - `RouterContract.Inputs.PopUntilAnnotation` + - `RouterContract.Inputs.PopUntilRoute` +- You can now access (and return) the default initial state from SavedState RestoreStateScope for cases where you want to + _optioannly_ restore the state, rather than recreating the default initial state yourself within the Adapter +- Debugger: APIs to deserialize +- Test: customize entire Builder before each test +- Changed Inputs and Events classes to be `sealed interface` instead of `sealed class`, and used data objects where + possible as subclasses of the Inputs or Events +- Added proper toString() values to Interceptors, for prettier logs diff --git a/examples/desktop/build.gradle.kts b/examples/desktop/build.gradle.kts index 33c2c628..f2c38609 100644 --- a/examples/desktop/build.gradle.kts +++ b/examples/desktop/build.gradle.kts @@ -36,6 +36,9 @@ kotlin { implementation(libs.kotlinx.coroutines.swing) implementation(libs.multiplatformSettings.core) implementation(libs.multiplatformSettings.noArg) + + implementation("io.github.oleksandrbalan:lazytable:1.5.0") + implementation("io.github.serpro69:kotlin-faker:1.14.0") } } } diff --git a/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/injector/ComposeDesktopInjector.kt b/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/injector/ComposeDesktopInjector.kt index b73161cb..8071cc5c 100644 --- a/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/injector/ComposeDesktopInjector.kt +++ b/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/injector/ComposeDesktopInjector.kt @@ -5,6 +5,7 @@ import com.copperleaf.ballast.examples.router.BallastExamples import com.copperleaf.ballast.examples.ui.bgg.BggViewModel import com.copperleaf.ballast.examples.ui.counter.CounterContract import com.copperleaf.ballast.examples.ui.counter.CounterViewModel +import com.copperleaf.ballast.examples.ui.datatable.DataTableViewModel import com.copperleaf.ballast.examples.ui.kitchensink.InputStrategySelection import com.copperleaf.ballast.examples.ui.kitchensink.KitchenSinkViewModel import com.copperleaf.ballast.examples.ui.scorekeeper.ScorekeeperViewModel @@ -75,4 +76,11 @@ interface ComposeDesktopInjector { coroutineScope: CoroutineScope, inputStrategy: InputStrategySelection, ): KitchenSinkViewModel + +// Storefront +// --------------------------------------------------------------------------------------------------------------------- + + fun storefrontViewModel( + coroutineScope: CoroutineScope, + ): DataTableViewModel } diff --git a/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/injector/ComposeDesktopInjectorImpl.kt b/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/injector/ComposeDesktopInjectorImpl.kt index d5e66fd5..11da7e8e 100644 --- a/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/injector/ComposeDesktopInjectorImpl.kt +++ b/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/injector/ComposeDesktopInjectorImpl.kt @@ -3,7 +3,9 @@ package com.copperleaf.ballast.examples.injector import androidx.compose.material.SnackbarHostState import com.copperleaf.ballast.BallastViewModelConfiguration import com.copperleaf.ballast.build +import com.copperleaf.ballast.core.BootstrapInterceptor import com.copperleaf.ballast.core.KillSwitch +import com.copperleaf.ballast.core.LifoInputStrategy import com.copperleaf.ballast.core.LoggingInterceptor import com.copperleaf.ballast.core.PrintlnLogger import com.copperleaf.ballast.debugger.BallastDebuggerClientConnection @@ -24,6 +26,9 @@ import com.copperleaf.ballast.examples.ui.counter.CounterContract import com.copperleaf.ballast.examples.ui.counter.CounterEventHandler import com.copperleaf.ballast.examples.ui.counter.CounterInputHandler import com.copperleaf.ballast.examples.ui.counter.CounterViewModel +import com.copperleaf.ballast.examples.ui.datatable.DataTableContract +import com.copperleaf.ballast.examples.ui.datatable.DataTableInputHandler +import com.copperleaf.ballast.examples.ui.datatable.DataTableViewModel import com.copperleaf.ballast.examples.ui.kitchensink.InputStrategySelection import com.copperleaf.ballast.examples.ui.kitchensink.KitchenSinkContract import com.copperleaf.ballast.examples.ui.kitchensink.KitchenSinkEventHandler @@ -156,7 +161,7 @@ class ComposeDesktopInjectorImpl( name = "Counter", ) .apply { - this += BallastDebuggerInterceptor.withJson( + this += BallastDebuggerInterceptor( debuggerConnection, inputsSerializer = CounterContract.Inputs.serializer(), eventsSerializer = CounterContract.Events.serializer(), @@ -297,6 +302,26 @@ class ComposeDesktopInjectorImpl( ) } + override fun storefrontViewModel(coroutineScope: CoroutineScope): DataTableViewModel { + return DataTableViewModel( + viewModelCoroutineScope = coroutineScope, + config = commonBuilder() + .apply { + this.inputStrategy = LifoInputStrategy() + } + .withViewModel( + initialState = DataTableContract.State(), + inputHandler = DataTableInputHandler(), + name = "Storefront", + ) + .apply { + this += BootstrapInterceptor { + DataTableContract.Inputs.Initialize + } + } + .build(), + ) + } // configs // --------------------------------------------------------------------------------------------------------------------- @@ -316,9 +341,9 @@ class ComposeDesktopInjectorImpl( .apply { this += LoggingInterceptor() logger = ::PrintlnLogger - if (debugger) { - this += BallastDebuggerInterceptor(debuggerConnection) - } +// if (debugger) { +// this += BallastDebuggerInterceptor(debuggerConnection) +// } } } } diff --git a/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/main.kt b/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/main.kt index 4f8d0bce..6397c9b2 100644 --- a/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/main.kt +++ b/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/main.kt @@ -19,10 +19,8 @@ import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowForward -import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier @@ -34,6 +32,7 @@ import com.copperleaf.ballast.examples.injector.ComposeDesktopInjectorImpl import com.copperleaf.ballast.examples.router.BallastExamples import com.copperleaf.ballast.examples.ui.bgg.BggUi import com.copperleaf.ballast.examples.ui.counter.CounterUi +import com.copperleaf.ballast.examples.ui.datatable.DataTableUi import com.copperleaf.ballast.examples.ui.kitchensink.InputStrategySelection import com.copperleaf.ballast.examples.ui.kitchensink.KitchenSinkUi import com.copperleaf.ballast.examples.ui.scorekeeper.ScorekeeperUi @@ -45,7 +44,6 @@ import com.copperleaf.ballast.navigation.routing.currentDestinationOrNull import com.copperleaf.ballast.navigation.routing.currentRouteOrNull import com.copperleaf.ballast.navigation.routing.directions import com.copperleaf.ballast.navigation.routing.optionalStringQuery -import okhttp3.Response import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi import org.jetbrains.compose.splitpane.HorizontalSplitPane import org.jetbrains.compose.splitpane.rememberSplitPaneState @@ -135,6 +133,14 @@ fun main() = singleWindowApplication(title = "Ballast Examples") { router::trySend, ) ) { Text("Kitchen Sink") } + ListItem( + modifier = Modifier + .routeLink( + BallastExamples.Storefront, + currentRoute, + router::trySend, + ) + ) { Text("Storefront") } } } } @@ -172,6 +178,10 @@ fun main() = singleWindowApplication(title = "Ballast Examples") { KitchenSinkUi.Content(injector, inputStrategySelection) } + BallastExamples.Storefront -> { + DataTableUi.Content(injector) + } + null -> {} } } diff --git a/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/router/BallastExamples.kt b/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/router/BallastExamples.kt index 23a141cf..2de18194 100644 --- a/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/router/BallastExamples.kt +++ b/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/router/BallastExamples.kt @@ -14,6 +14,7 @@ enum class BallastExamples( Undo("/examples/undo"), ApiCall("/examples/api-call"), KitchenSink("/examples/kitchen-sink?inputStrategy={?}"), + Storefront("/examples/shop"), ; override val matcher: RouteMatcher = RouteMatcher.create(routeFormat) diff --git a/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/router/RouterSavedStateAdapter.kt b/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/router/RouterSavedStateAdapter.kt index 889b2cfe..4c393a27 100644 --- a/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/router/RouterSavedStateAdapter.kt +++ b/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/router/RouterSavedStateAdapter.kt @@ -33,16 +33,12 @@ public class RouterSavedStateAdapter( RouterContract.Events, RouterContract.State> { - public interface Prefs { - var backstackUrls: List - } - override suspend fun SaveStateScope< RouterContract.Inputs, RouterContract.Events, RouterContract.State>.save() { saveAll { backstack -> - prefs.backstack = backstack.map { it.originalDestinationUrl } + prefs.backstack = backstack.map { it.originalDestinationUrl }.takeLast(5) } } diff --git a/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/DataTableContract.kt b/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/DataTableContract.kt new file mode 100644 index 00000000..564ebf02 --- /dev/null +++ b/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/DataTableContract.kt @@ -0,0 +1,45 @@ +package com.copperleaf.ballast.examples.ui.datatable + +import com.copperleaf.ballast.examples.ui.datatable.models.ColumnSort +import com.copperleaf.ballast.examples.ui.datatable.models.Product +import com.copperleaf.ballast.examples.ui.datatable.models.ProductColumn +import com.copperleaf.ballast.examples.ui.datatable.models.SortDirection + +public object DataTableContract { + public data class State( + val loading: Boolean = false, + + val products: List = emptyList(), + val filteredProducts: List = emptyList(), + + val searchQuery: String = "", + val filterByTags: List = emptyList(), + val filterInStock: Boolean = false, + val priceRange: ClosedRange = 0u..UInt.MAX_VALUE, + val filterByRating: UInt? = null, + val sortResultsBy: List = listOf(ColumnSort(ProductColumn.Name, SortDirection.Ascending)), + ) { + override fun toString(): String { + return "State(searchQuery='$searchQuery', filterByTags=$filterByTags, filterInStock=$filterInStock, priceRange=$priceRange, filterByRating=$filterByRating, sortResultsBy=$sortResultsBy)" + } + } + + public sealed class Inputs { + data object Initialize : Inputs() + + data class UpdateSearchQuery(val searchQuery: String): Inputs() + + data class ToggleColumnSort(val column: ProductColumn): Inputs() + data class ToggleTag(val tag: String): Inputs() + data object ToggleFilterInStock: Inputs() + + data class UpdatePriceRangeMin(val minPrice: UInt): Inputs() + data class UpdatePriceRangeMax(val maxPrice: UInt): Inputs() + data class UpdateRating(val rating: UInt): Inputs() + + data object ApplyFilters: Inputs() + } + + public sealed class Events { + } +} diff --git a/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/DataTableInputHandler.kt b/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/DataTableInputHandler.kt new file mode 100644 index 00000000..f39d3d86 --- /dev/null +++ b/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/DataTableInputHandler.kt @@ -0,0 +1,123 @@ +package com.copperleaf.ballast.examples.ui.datatable + +import com.copperleaf.ballast.InputHandler +import com.copperleaf.ballast.InputHandlerScope +import com.copperleaf.ballast.examples.ui.datatable.models.ColumnSort +import com.copperleaf.ballast.examples.ui.datatable.models.SortDirection +import com.copperleaf.ballast.examples.ui.datatable.utils.generateProducts +import com.copperleaf.ballast.examples.ui.datatable.utils.queryProducts +import com.copperleaf.ballast.postInput + +public class DataTableInputHandler : InputHandler< + DataTableContract.Inputs, + DataTableContract.Events, + DataTableContract.State> { + override suspend fun InputHandlerScope< + DataTableContract.Inputs, + DataTableContract.Events, + DataTableContract.State>.handleInput( + input: DataTableContract.Inputs + ): Unit = when (input) { + is DataTableContract.Inputs.Initialize -> { + updateState { it.copy(loading = true) } + val products = generateProducts() + updateState { it.copy(loading = false, products = products, filteredProducts = products) } + postInput(DataTableContract.Inputs.ApplyFilters) + } + + is DataTableContract.Inputs.UpdateSearchQuery -> { + updateState { + it.copy( + searchQuery = input.searchQuery + ) + } + postInput(DataTableContract.Inputs.ApplyFilters) + } + + is DataTableContract.Inputs.ToggleColumnSort -> { + updateState { + it.copy( + sortResultsBy = it + .sortResultsBy + .toMutableList() + .apply { + val sortColumnIndex = indexOfFirst { it.column == input.column } + val sortColumnCurrentDirection = this.getOrNull(sortColumnIndex)?.sortDirection + when (sortColumnCurrentDirection) { + null -> { + // no sort on this colum yet. Add it sorting ascending at the end + add(ColumnSort(input.column, SortDirection.Ascending)) + } + + SortDirection.Ascending -> { + // existing sort was ascending, switch it to descending + this[sortColumnIndex] = ColumnSort(input.column, SortDirection.Descending) + } + + SortDirection.Descending -> { + // existing sort was descending, remove it from the sort + removeAt(sortColumnIndex) + } + } + } + .toList() + ) + } + postInput(DataTableContract.Inputs.ApplyFilters) + } + + is DataTableContract.Inputs.ToggleTag -> { + updateState { + it.copy( + filterByTags = it + .filterByTags + .toMutableList() + .apply { + if (input.tag in this) { + remove(input.tag) + } else { + add(input.tag) + } + } + .toList() + ) + } + postInput(DataTableContract.Inputs.ApplyFilters) + } + + is DataTableContract.Inputs.ToggleFilterInStock -> { + updateState { it.copy(filterInStock = !it.filterInStock) } + postInput(DataTableContract.Inputs.ApplyFilters) + } + + is DataTableContract.Inputs.UpdatePriceRangeMin -> { + updateState { it.copy(priceRange = input.minPrice..it.priceRange.endInclusive) } + postInput(DataTableContract.Inputs.ApplyFilters) + } + + is DataTableContract.Inputs.UpdatePriceRangeMax -> { + updateState { it.copy(priceRange = it.priceRange.start..input.maxPrice) } + postInput(DataTableContract.Inputs.ApplyFilters) + } + + is DataTableContract.Inputs.UpdateRating -> { + updateState { it.copy(filterByRating = input.rating) } + postInput(DataTableContract.Inputs.ApplyFilters) + } + + is DataTableContract.Inputs.ApplyFilters -> { + val currentState = updateStateAndGet { it.copy(loading = true) } + val filteredProducts = queryProducts( + products = currentState.products, + searchQuery = currentState.searchQuery.takeIf { it.isNotEmpty() }, + filterByTags = currentState.filterByTags.takeIf { it.isNotEmpty() }, + filterInStock = currentState.filterInStock, + priceRange = currentState.priceRange.takeIf { it.start != 0u || it.endInclusive != UInt.MAX_VALUE }, + rating = currentState.filterByRating, + sortResultsBy = currentState.sortResultsBy, + ) + + updateState { it.copy(loading = false, filteredProducts = filteredProducts) } + } + } +} diff --git a/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/DataTableUi.kt b/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/DataTableUi.kt new file mode 100644 index 00000000..961b6a2e --- /dev/null +++ b/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/DataTableUi.kt @@ -0,0 +1,282 @@ +package com.copperleaf.ballast.examples.ui.datatable + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.selection.toggleable +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Checkbox +import androidx.compose.material.Chip +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Icon +import androidx.compose.material.ListItem +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.RadioButton +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.ArrowDropUp +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.copperleaf.ballast.examples.injector.ComposeDesktopInjector +import com.copperleaf.ballast.examples.ui.datatable.models.Product +import com.copperleaf.ballast.examples.ui.datatable.models.ProductColumn +import com.copperleaf.ballast.examples.ui.datatable.models.SortDirection +import eu.wewox.lazytable.LazyTable +import eu.wewox.lazytable.LazyTableItem +import eu.wewox.lazytable.lazyTableDimensions + +object DataTableUi { + + @Composable + fun Content(injector: ComposeDesktopInjector) { + val viewModelCoroutineScope = rememberCoroutineScope() + val vm = remember(viewModelCoroutineScope) { + injector.storefrontViewModel(viewModelCoroutineScope) + } + val uiState by vm.observeStates().collectAsState() + + Content(uiState) { vm.trySend(it) } + } + + @OptIn(ExperimentalLayoutApi::class) + @Composable + public fun Content( + uiState: DataTableContract.State, + postInput: (DataTableContract.Inputs) -> Unit, + ) { + Column(Modifier.fillMaxSize()) { + TopAppBar(title = { Text("Undo/Redo") }) + Row( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .verticalScroll(rememberScrollState()), + ) { + Column(Modifier.width(240.dp)) { + if (uiState.loading) { + CircularProgressIndicator() + } + + var searchText by remember { mutableStateOf("") } + OutlinedTextField( + value = searchText, + onValueChange = { + searchText = it + postInput(DataTableContract.Inputs.UpdateSearchQuery(it)) + }, + label = { Text("Search for coffees") } + ) + + ListItem( + modifier = Modifier + .toggleable( + value = uiState.filterInStock, + onValueChange = { postInput(DataTableContract.Inputs.ToggleFilterInStock) } + ), + icon = { + Checkbox( + checked = uiState.filterInStock, + onCheckedChange = null + ) + }, + text = { Text("Show In Stock Only") } + ) + + OutlinedTextField( + value = uiState.priceRange.start.takeIf { it != 0u }?.toString() ?: "", + onValueChange = { + it.toUIntOrNull()?.let { intValue -> + postInput(DataTableContract.Inputs.UpdatePriceRangeMin(intValue)) + } + }, + label = { Text("min price") } + ) + OutlinedTextField( + value = uiState.priceRange.endInclusive.takeIf { it != UInt.MAX_VALUE }?.toString() ?: "", + onValueChange = { + it.toUIntOrNull()?.let { intValue -> + postInput(DataTableContract.Inputs.UpdatePriceRangeMax(intValue)) + } + }, + label = { Text("max price") } + ) + + Text("Filter By Rating") + Row { + (1u..5u).map { ratingButtonValue -> + Column { + Text("$ratingButtonValue stars") + RadioButton( + selected = uiState.filterByRating != null && uiState.filterByRating >= ratingButtonValue, + onClick = { + postInput(DataTableContract.Inputs.UpdateRating(ratingButtonValue)) + }, + ) + } + } + } + + Text("Filter By Tag") + FlowRow { + uiState.filterByTags.forEach { tag -> + Chip( + onClick = { postInput(DataTableContract.Inputs.ToggleTag(tag)) }, + content = { Text(tag) }, + ) + } + } + + Text("${uiState.filteredProducts.size} / ${uiState.products.size} products visible") + } + Column(Modifier.weight(1f)) { + if (uiState.filteredProducts.isEmpty()) { + Text("No products") + } else { + val headerRow: List> = ProductColumn + .entries + .map { column -> + Triple(0, column, null) + } + + val productRows: List> = uiState + .filteredProducts + .flatMapIndexed { rowIndex, product -> + ProductColumn + .entries + .map { column -> + Triple(rowIndex + 1, column, product) + } + } + + val cells: List> = headerRow + productRows + + LazyTable( + modifier = Modifier.fillMaxSize(), + dimensions = lazyTableDimensions( + columnSize = { + when(ProductColumn.entries[it]) { + ProductColumn.Name -> 180.dp + ProductColumn.Description -> 240.dp + ProductColumn.Tags -> 300.dp + ProductColumn.Quantity -> 96.dp + ProductColumn.Cost -> 96.dp + ProductColumn.Rating -> 96.dp + ProductColumn.NumberOfReviews -> 96.dp + } + }, + rowSize = { + 96.dp + }, + ) + ) { + items( + items = cells, + layoutInfo = { (rowIndex, column, _) -> + LazyTableItem( + column = column.ordinal, + row = rowIndex, + ) + } + ) { (_, column, product) -> + if (product != null) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .background(MaterialTheme.colors.surface) + .border(Dp.Hairline, MaterialTheme.colors.onSurface) + ) { + when (column) { + ProductColumn.Name -> { + Text(product.name + "") + } + + ProductColumn.Description -> { + Text(product.description + "") + } + + ProductColumn.Tags -> { + FlowRow { + product.tags.forEach { tag -> + Chip( + onClick = { postInput(DataTableContract.Inputs.ToggleTag(tag)) }, + content = { Text(tag) }, + ) + } + } + } + + ProductColumn.Quantity -> { + Text("${product.quantity}", maxLines = 1) + } + + ProductColumn.Cost -> { + Text("$${product.cost}.00", maxLines = 1) + } + + ProductColumn.Rating -> { + Text("${product.rating} / 5", maxLines = 1) + } + + ProductColumn.NumberOfReviews -> { + Text("${product.numberOfReviews}", maxLines = 1) + } + } + } + } else { + Row( + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .background(MaterialTheme.colors.surface) + .border(Dp.Hairline, MaterialTheme.colors.onSurface) + .clickable { + postInput(DataTableContract.Inputs.ToggleColumnSort(column)) + } + ) { + Text(column.name) + + if(column.canSort) { + val sortColumnIndex = uiState.sortResultsBy.indexOfFirst { it.column == column } + val sortColumnCurrentDirection = uiState.sortResultsBy.getOrNull(sortColumnIndex)?.sortDirection + val icon = when (sortColumnCurrentDirection) { + null -> null + SortDirection.Ascending -> Icons.Default.ArrowDropDown + SortDirection.Descending -> Icons.Default.ArrowDropUp + } + + if(icon != null) { + Icon(icon, "") + } + } + } + } + } + } + } + } + } + } + } +} diff --git a/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/DataTableViewModel.kt b/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/DataTableViewModel.kt new file mode 100644 index 00000000..9a9f2481 --- /dev/null +++ b/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/DataTableViewModel.kt @@ -0,0 +1,21 @@ +package com.copperleaf.ballast.examples.ui.datatable + +import com.copperleaf.ballast.BallastViewModelConfiguration +import com.copperleaf.ballast.core.BasicViewModel +import com.copperleaf.ballast.eventHandler +import kotlinx.coroutines.CoroutineScope + +class DataTableViewModel( + viewModelCoroutineScope: CoroutineScope, + config: BallastViewModelConfiguration< + DataTableContract.Inputs, + DataTableContract.Events, + DataTableContract.State>, +) : BasicViewModel< + DataTableContract.Inputs, + DataTableContract.Events, + DataTableContract.State>( + config = config, + eventHandler = eventHandler { }, + coroutineScope = viewModelCoroutineScope, +) diff --git a/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/models/ColumnSort.kt b/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/models/ColumnSort.kt new file mode 100644 index 00000000..3ebd6f98 --- /dev/null +++ b/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/models/ColumnSort.kt @@ -0,0 +1,10 @@ +package com.copperleaf.ballast.examples.ui.datatable.models + +public data class ColumnSort( + val column: ProductColumn, + val sortDirection: SortDirection, +) { + companion object { + val DefaultSort = ColumnSort(ProductColumn.Name, SortDirection.Ascending) + } +} diff --git a/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/models/Product.kt b/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/models/Product.kt new file mode 100644 index 00000000..8755f961 --- /dev/null +++ b/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/models/Product.kt @@ -0,0 +1,14 @@ +package com.copperleaf.ballast.examples.ui.datatable.models + +public data class Product( + val name: String, + val brand: String, + val description: String, + val tags: List, + + val quantity: UInt, + val cost: UInt, + + val rating: Double, + val numberOfReviews: UInt, +) diff --git a/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/models/ProductColumn.kt b/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/models/ProductColumn.kt new file mode 100644 index 00000000..48a8a1c3 --- /dev/null +++ b/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/models/ProductColumn.kt @@ -0,0 +1,11 @@ +package com.copperleaf.ballast.examples.ui.datatable.models + +public enum class ProductColumn(val canSort: Boolean) { + Name(canSort = true), + Description(canSort = true), + Tags(canSort = false), + Quantity(canSort = true), + Cost(canSort = true), + Rating(canSort = true), + NumberOfReviews(canSort = true), +} diff --git a/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/models/SortDirection.kt b/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/models/SortDirection.kt new file mode 100644 index 00000000..634edd31 --- /dev/null +++ b/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/models/SortDirection.kt @@ -0,0 +1,6 @@ +package com.copperleaf.ballast.examples.ui.datatable.models + +public enum class SortDirection { + Ascending, + Descending, +} diff --git a/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/utils/generateProducts.kt b/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/utils/generateProducts.kt new file mode 100644 index 00000000..a78b411d --- /dev/null +++ b/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/utils/generateProducts.kt @@ -0,0 +1,42 @@ +package com.copperleaf.ballast.examples.ui.datatable.utils + +import com.copperleaf.ballast.examples.ui.datatable.models.Product +import io.github.serpro69.kfaker.Faker +import io.github.serpro69.kfaker.fakerConfig +import kotlinx.coroutines.delay +import kotlin.random.Random +import kotlin.random.nextUInt +import kotlin.time.Duration.Companion.seconds + +public suspend fun generateProducts( + random: Random = Random, + faker: Faker = Faker(fakerConfig {}) +): List { + delay(1.5.seconds) + + val numberOfItems = random.nextInt(50, 100) + + return List(numberOfItems) { + generateProduct(random, faker) + } +} + +public fun generateProduct(random: Random, faker: Faker): Product { + return Product( + name = faker.coffee.blendName(), + brand = faker.coffee.country(), + description = faker.coffee.notes(), + tags = (listOf(faker.coffee.country()) + faker.coffee.notes().split(" ")).sorted(), + + quantity = if (random.nextInt(0, 100) <= 60) { + // products have a 60% change of being in stock + random.nextUInt(1u, 100u) + } else { + 0u + }, + cost = random.nextUInt(1u, 30u), + + rating = random.nextInt(0, 4) + (random.nextInt(0, 10) / 10.0), + numberOfReviews = random.nextUInt(0u, 1000u), + ) +} diff --git a/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/utils/query.kt b/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/utils/query.kt new file mode 100644 index 00000000..e0cd9fa5 --- /dev/null +++ b/examples/desktop/src/jvmMain/kotlin/com/copperleaf/ballast/examples/ui/datatable/utils/query.kt @@ -0,0 +1,100 @@ +package com.copperleaf.ballast.examples.ui.datatable.utils + +import com.copperleaf.ballast.examples.ui.datatable.models.ColumnSort +import com.copperleaf.ballast.examples.ui.datatable.models.Product +import com.copperleaf.ballast.examples.ui.datatable.models.ProductColumn +import com.copperleaf.ballast.examples.ui.datatable.models.SortDirection +import kotlinx.coroutines.delay +import kotlin.time.Duration.Companion.seconds + +public suspend fun queryProducts( + products: List, + searchQuery: String?, + filterByTags: List?, + filterInStock: Boolean, + + priceRange: ClosedRange?, + rating: UInt?, + sortResultsBy: List, +): List { + delay(1.5.seconds) + + return products + .asSequence() + .filterIfPresent(searchQuery) { _searchQuery, product -> product.matchesSearchQuery(_searchQuery) } + .filterIfPresent(filterByTags) { _filterByTags, product -> product.containsAllTags(_filterByTags) } + .filterIfTrue(filterInStock) { product -> product.isInStock() } + .filterIfPresent(priceRange) { _priceRange, product -> product.priceIsInRange(_priceRange) } + .filterIfPresent(rating) { _rating, product -> product.ratingIsInRange(_rating) } + .sortedWith(sortResultsBy.asComparator()) + .toList() +} + +private fun Product.matchesSearchQuery(searchQuery: String): Boolean { + return this.name.contains(searchQuery) || + this.description.contains(searchQuery) +} + +private fun Product.containsAllTags(tags: List): Boolean { + val productSortedTags = this.tags.sorted() + val fitlerSortedTags = tags.sorted() + return productSortedTags.containsAll(fitlerSortedTags) +} + +private fun Product.isInStock(): Boolean { + return this.quantity > 0u +} + +private fun Product.priceIsInRange(priceRange: ClosedRange): Boolean { + return this.cost in priceRange +} + +private fun Product.ratingIsInRange(rating: UInt): Boolean { + return this.rating > rating.toDouble() +} + +private fun List.asComparator(): Comparator { + if(this.isEmpty()) { + return ColumnSort.DefaultSort.asComparator() + } + + return this + .filter { it.column.canSort } + .map { it.asComparator() } + .reduce { acc, comparator -> acc.thenComparing(comparator) } +} + +private fun ColumnSort.asComparator() : Comparator { + val propertySelector = { product: Product -> + when(this.column) { + ProductColumn.Name -> product.name + ProductColumn.Description -> product.description + ProductColumn.Tags -> error("cannot sort by tags") + ProductColumn.Quantity -> product.quantity + ProductColumn.Cost -> product.cost + ProductColumn.Rating -> product.rating + ProductColumn.NumberOfReviews -> product.numberOfReviews + } + } + + return when (sortDirection) { + SortDirection.Ascending -> compareBy(propertySelector) + SortDirection.Descending -> compareByDescending((propertySelector)) + } +} + +private fun Sequence.filterIfPresent(filterContext: U?, predicate: (U, T) -> Boolean): Sequence { + return if (filterContext != null) { + filter { predicate(filterContext, it) } + } else { + this + } +} + +private fun Sequence.filterIfTrue(shouldApplyFilter: Boolean, predicate: (T) -> Boolean): Sequence { + return if (shouldApplyFilter) { + filter { predicate(it) } + } else { + this + } +} diff --git a/examples/web/src/jsMain/kotlin/com/copperleaf/ballast/examples/injector/ComposeWebInjectorImpl.kt b/examples/web/src/jsMain/kotlin/com/copperleaf/ballast/examples/injector/ComposeWebInjectorImpl.kt index 166ca93c..d69a9d09 100644 --- a/examples/web/src/jsMain/kotlin/com/copperleaf/ballast/examples/injector/ComposeWebInjectorImpl.kt +++ b/examples/web/src/jsMain/kotlin/com/copperleaf/ballast/examples/injector/ComposeWebInjectorImpl.kt @@ -129,7 +129,7 @@ class ComposeWebInjectorImpl( name = "Counter", ) .apply { - this += BallastDebuggerInterceptor.withJson( + this += BallastDebuggerInterceptor( debuggerConnection, inputsSerializer = CounterContract.Inputs.serializer(), eventsSerializer = CounterContract.Events.serializer(), diff --git a/gradle-convention-plugins b/gradle-convention-plugins index cd065464..31dd1bb8 160000 --- a/gradle-convention-plugins +++ b/gradle-convention-plugins @@ -1 +1 @@ -Subproject commit cd0654640d2689e793e86aedafa689430fab113a +Subproject commit 31dd1bb8a05f3c9b7f291fe94e762604884c2bcd diff --git a/settings.gradle.kts b/settings.gradle.kts index 66d1de1c..f79fbc0c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -6,7 +6,7 @@ pluginManagement { } } -val conventionDir = "./gradle-convention-plugins" +val conventionDir = "./../gradle-convention-plugins" dependencyResolutionManagement { versionCatalogs {