diff --git a/.gitignore b/.gitignore
index 8d9ef2c09..a0e81a534 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,4 +12,27 @@ buildNumber.properties
.mvn/wrapper/maven-wrapper.jar
# LibreOffice lock files (.rgg especially)
-.~lock*
\ No newline at end of file
+.~lock*
+
+# https://github.com/github/gitignore/blob/main/Gradle.gitignore
+.gradle
+**/build/
+!src/**/build/
+
+# Ignore Gradle GUI config
+gradle-app.setting
+
+# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
+!gradle-wrapper.jar
+
+# Avoid ignore Gradle wrappper properties
+!gradle-wrapper.properties
+
+# Cache of project
+.gradletasknamecache
+
+# Eclipse Gradle plugin generated files
+# Eclipse Core
+.project
+# JDT-specific (Eclipse Java Development Tools)
+.classpath
\ No newline at end of file
diff --git a/admin/pom.xml b/admin/pom.xml
index 73039cf7b..230156de3 100644
--- a/admin/pom.xml
+++ b/admin/pom.xml
@@ -3,10 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
- ../pom.xml
+ ../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/api-client-motorsportreg-test/pom.xml b/api-client-motorsportreg-test/pom.xml
index d1203ff18..2c84d123f 100644
--- a/api-client-motorsportreg-test/pom.xml
+++ b/api-client-motorsportreg-test/pom.xml
@@ -3,9 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
+ ../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/api-client-motorsportreg/pom.xml b/api-client-motorsportreg/pom.xml
index b759669e8..b0a91e0e9 100644
--- a/api-client-motorsportreg/pom.xml
+++ b/api-client-motorsportreg/pom.xml
@@ -3,9 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
+ ../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/buildsrc/buildsrc-kotlin-parent/pom.xml b/buildsrc/buildsrc-kotlin-parent/pom.xml
new file mode 100644
index 000000000..1c6ffeb24
--- /dev/null
+++ b/buildsrc/buildsrc-kotlin-parent/pom.xml
@@ -0,0 +1,130 @@
+
+
+ 4.0.0
+
+ tech.coner.trailer
+ parent
+ 0.1.0-SNAPSHOT
+ ../../pom.xml
+
+
+ buildsrc-kotlin-parent
+
+ pom
+
+
+ ../../core
+ ../../core-test
+ ../../datasource-crispy-fish
+ ../../datasource-crispy-fish-test
+ ../../datasource-snoozle
+ ../../api-client-motorsportreg
+ ../../api-client-motorsportreg-test
+ ../../datasource-motorsportreg
+ ../../io
+ ../../io-kodein-di
+ ../../admin
+ ../../io-test
+ ../../presentation/presentation
+ ../../presentation/presentation-text
+ ../../presentation/presentation-all-kodein-di
+ ../../presentation/presentation-json
+ ../../io-kodein-di-test
+ ../../io-testsupport
+ ../../core-kodein-di
+ ../../core-kodein-di-test
+ ../../webapp-competition
+ ../../testutil/assertk-ktor
+ ../../presentation/presentation-all-testsupport-mockk
+ ../../toolkit/konstraints
+ ../../toolkit/presentation/presentation
+ ../../toolkit/presentation/presentation-test
+ ../../toolkit/presentation/presentation-testsupport
+ ../../toolkit/samples/samples-common
+ ../../toolkit/validation/validation
+ ../../toolkit/validation/validation-testsupport
+ ../../toolkit/util
+
+
+
+ UTF-8
+ official
+ 1.8
+
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-core
+
+
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-test-jvm
+
+
+ org.junit.jupiter
+ junit-jupiter
+
+
+ com.willowtreeapps.assertk
+ assertk-jvm
+
+
+ io.mockk
+ mockk-jvm
+
+
+
+
+
+ mavenCentral
+ https://repo1.maven.org/maven2/
+
+
+
+
+ src/main/kotlin
+ src/test/kotlin
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+
+
+ compile
+ compile
+
+ compile
+
+
+
+ test-compile
+ test-compile
+
+ test-compile
+
+
+
+
+
+ maven-surefire-plugin
+
+
+ maven-failsafe-plugin
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+
+
+
+
+
\ No newline at end of file
diff --git a/core-kodein-di-test/pom.xml b/core-kodein-di-test/pom.xml
index 6536a8b97..209d8fe0d 100644
--- a/core-kodein-di-test/pom.xml
+++ b/core-kodein-di-test/pom.xml
@@ -3,9 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
+ ../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/core-kodein-di/pom.xml b/core-kodein-di/pom.xml
index c6cda81b8..ed520e126 100644
--- a/core-kodein-di/pom.xml
+++ b/core-kodein-di/pom.xml
@@ -3,9 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
+ ../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/core-test/pom.xml b/core-test/pom.xml
index 931e8f837..943e0b884 100644
--- a/core-test/pom.xml
+++ b/core-test/pom.xml
@@ -3,10 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
- ../pom.xml
+ ../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/core/pom.xml b/core/pom.xml
index 510d00867..382c99b2d 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -4,9 +4,9 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
tech.coner.trailer
- parent
+ buildsrc-kotlin-parent
0.1.0-SNAPSHOT
- ../pom.xml
+ ../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/datasource-crispy-fish-test/pom.xml b/datasource-crispy-fish-test/pom.xml
index 598c0359d..05947b890 100644
--- a/datasource-crispy-fish-test/pom.xml
+++ b/datasource-crispy-fish-test/pom.xml
@@ -3,9 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
+ ../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/datasource-crispy-fish/pom.xml b/datasource-crispy-fish/pom.xml
index 35e5dcb7a..abf504de2 100644
--- a/datasource-crispy-fish/pom.xml
+++ b/datasource-crispy-fish/pom.xml
@@ -4,9 +4,9 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
tech.coner.trailer
- parent
+ buildsrc-kotlin-parent
0.1.0-SNAPSHOT
- ../pom.xml
+ ../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/datasource-motorsportreg/pom.xml b/datasource-motorsportreg/pom.xml
index 0518e08b0..0292e37f4 100644
--- a/datasource-motorsportreg/pom.xml
+++ b/datasource-motorsportreg/pom.xml
@@ -3,9 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
+ ../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/datasource-snoozle/pom.xml b/datasource-snoozle/pom.xml
index 7692f51ba..e4966c200 100644
--- a/datasource-snoozle/pom.xml
+++ b/datasource-snoozle/pom.xml
@@ -3,10 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
- ../pom.xml
+ ../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/io-kodein-di-test/pom.xml b/io-kodein-di-test/pom.xml
index ebc1c305d..312c5edcd 100644
--- a/io-kodein-di-test/pom.xml
+++ b/io-kodein-di-test/pom.xml
@@ -3,9 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
+ ../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/io-kodein-di/pom.xml b/io-kodein-di/pom.xml
index 887fa16f7..87d66b25f 100644
--- a/io-kodein-di/pom.xml
+++ b/io-kodein-di/pom.xml
@@ -3,9 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
+ ../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/io-test/pom.xml b/io-test/pom.xml
index 552a2d4bd..7c79c8b42 100644
--- a/io-test/pom.xml
+++ b/io-test/pom.xml
@@ -3,9 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
+ ../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/io-testsupport/pom.xml b/io-testsupport/pom.xml
index bd4c00fb9..6199df9bd 100644
--- a/io-testsupport/pom.xml
+++ b/io-testsupport/pom.xml
@@ -3,9 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
+ ../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/io/pom.xml b/io/pom.xml
index 6d447f27c..3fe395aac 100644
--- a/io/pom.xml
+++ b/io/pom.xml
@@ -3,10 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
tech.coner.trailer
+ buildsrc-kotlin-parent
0.1.0-SNAPSHOT
- ../pom.xml
+ ../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/pom.xml b/pom.xml
index 12162456b..df0c2e0b3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -10,50 +10,23 @@
0.1.0-SNAPSHOT
- core
- core-test
- datasource-crispy-fish
- datasource-crispy-fish-test
- datasource-snoozle
- api-client-motorsportreg
- api-client-motorsportreg-test
- datasource-motorsportreg
- io
- io-kodein-di
- admin
- io-test
- presentation/presentation
- presentation/presentation-text
- presentation/presentation-all-kodein-di
- presentation/presentation-json
- io-kodein-di-test
- io-testsupport
- core-kodein-di
- core-kodein-di-test
- webapp-competition
- testutil/assertk-ktor
- presentation/presentation-all-testsupport-mockk
- toolkit/konstraints
- toolkit/presentation/presentation
- toolkit/presentation/presentation-test
- toolkit/presentation/presentation-testsupport
- toolkit/samples/samples-common
- toolkit/validation/validation
- toolkit/validation/validation-testsupport
- toolkit/util
+ buildsrc/buildsrc-kotlin-parent
+ toolkit/samples/dmvapp/dmvapp-gui
+ ${project.version}
UTF-8
17
${maven.compiler.source}
VERBOSE
+ 3.7.1
3.12.1
3.3.1
3.2.3
3.6.0
1.2.2
- 1.9.22
+ 2.0.0
2.0
true
official
@@ -79,39 +52,11 @@
1.0.0
-
-
- org.jetbrains.kotlin
- kotlin-stdlib-jdk8
-
-
- org.jetbrains.kotlinx
- kotlinx-coroutines-core
-
-
-
- org.jetbrains.kotlinx
- kotlinx-coroutines-test-jvm
-
-
- org.junit.jupiter
- junit-jupiter
-
-
- com.willowtreeapps.assertk
- assertk-jvm
-
-
- io.mockk
- mockk-jvm
-
-
-
org.jetbrains.kotlin
- kotlin-stdlib-jdk8
+ kotlin-stdlib
${kotlin.version}
@@ -274,26 +219,6 @@
- ${project.basedir}/src/main/kotlin
- ${project.basedir}/src/test/kotlin
-
-
-
- org.jetbrains.kotlin
- kotlin-maven-plugin
-
-
- maven-surefire-plugin
-
-
- maven-failsafe-plugin
-
-
- org.codehaus.mojo
- exec-maven-plugin
-
-
-
@@ -343,6 +268,11 @@
maven-resources-plugin
${maven-resources-plugin.version}
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+ ${maven-dependency-plugin.version}
+
org.apache.maven.plugins
maven-compiler-plugin
diff --git a/presentation/presentation-all-kodein-di/pom.xml b/presentation/presentation-all-kodein-di/pom.xml
index 82daec4d0..86f075999 100644
--- a/presentation/presentation-all-kodein-di/pom.xml
+++ b/presentation/presentation-all-kodein-di/pom.xml
@@ -3,10 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
- ../../pom.xml
+ ../../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/presentation/presentation-all-testsupport-mockk/pom.xml b/presentation/presentation-all-testsupport-mockk/pom.xml
index 890bd23e8..7840672e9 100644
--- a/presentation/presentation-all-testsupport-mockk/pom.xml
+++ b/presentation/presentation-all-testsupport-mockk/pom.xml
@@ -5,9 +5,9 @@
4.0.0
tech.coner.trailer
- parent
+ buildsrc-kotlin-parent
0.1.0-SNAPSHOT
- ../../pom.xml
+ ../../buildsrc/buildsrc-kotlin-parent/pom.xml
presentation-all-testsupport-mockk
diff --git a/presentation/presentation-json/pom.xml b/presentation/presentation-json/pom.xml
index 5947e1a9d..58afb75b0 100644
--- a/presentation/presentation-json/pom.xml
+++ b/presentation/presentation-json/pom.xml
@@ -3,10 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
- ../../pom.xml
+ ../../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/presentation/presentation-text/pom.xml b/presentation/presentation-text/pom.xml
index 0eb9938fd..7ab27caff 100644
--- a/presentation/presentation-text/pom.xml
+++ b/presentation/presentation-text/pom.xml
@@ -3,10 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
- ../../pom.xml
+ ../../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/presentation/presentation/pom.xml b/presentation/presentation/pom.xml
index e967d4ba9..89306fab7 100644
--- a/presentation/presentation/pom.xml
+++ b/presentation/presentation/pom.xml
@@ -3,10 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
- ../../pom.xml
+ ../../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/render-html/pom.xml b/render-html/pom.xml
index 31ec9fe1b..9282dbbf5 100644
--- a/render-html/pom.xml
+++ b/render-html/pom.xml
@@ -3,7 +3,7 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
diff --git a/testutil/assertk-ktor/pom.xml b/testutil/assertk-ktor/pom.xml
index bfcefa303..4d17d351c 100644
--- a/testutil/assertk-ktor/pom.xml
+++ b/testutil/assertk-ktor/pom.xml
@@ -3,10 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
- ../../pom.xml
+ ../../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0
diff --git a/toolkit/konstraints/pom.xml b/toolkit/konstraints/pom.xml
index dfe701a17..4b77739c1 100644
--- a/toolkit/konstraints/pom.xml
+++ b/toolkit/konstraints/pom.xml
@@ -5,9 +5,9 @@
4.0.0
tech.coner.trailer
- parent
+ buildsrc-kotlin-parent
0.1.0-SNAPSHOT
- ../../pom.xml
+ ../../buildsrc/buildsrc-kotlin-parent/pom.xml
toolkit-konstraints
diff --git a/toolkit/presentation/presentation-test/pom.xml b/toolkit/presentation/presentation-test/pom.xml
index b02d36080..8b3c08095 100644
--- a/toolkit/presentation/presentation-test/pom.xml
+++ b/toolkit/presentation/presentation-test/pom.xml
@@ -5,9 +5,9 @@
4.0.0
tech.coner.trailer
- parent
+ buildsrc-kotlin-parent
0.1.0-SNAPSHOT
- ../../../pom.xml
+ ../../../buildsrc/buildsrc-kotlin-parent/pom.xml
toolkit-presentation-test
diff --git a/toolkit/presentation/presentation-testsupport/pom.xml b/toolkit/presentation/presentation-testsupport/pom.xml
index 2419cb405..90670303e 100644
--- a/toolkit/presentation/presentation-testsupport/pom.xml
+++ b/toolkit/presentation/presentation-testsupport/pom.xml
@@ -5,9 +5,9 @@
4.0.0
tech.coner.trailer
- parent
+ buildsrc-kotlin-parent
0.1.0-SNAPSHOT
- ../../../pom.xml
+ ../../../buildsrc/buildsrc-kotlin-parent/pom.xml
toolkit-presentation-testsupport
diff --git a/toolkit/presentation/presentation/pom.xml b/toolkit/presentation/presentation/pom.xml
index 6c412d6d5..7431aca8a 100644
--- a/toolkit/presentation/presentation/pom.xml
+++ b/toolkit/presentation/presentation/pom.xml
@@ -5,9 +5,9 @@
4.0.0
tech.coner.trailer
- parent
+ buildsrc-kotlin-parent
0.1.0-SNAPSHOT
- ../../../pom.xml
+ ../../../buildsrc/buildsrc-kotlin-parent/pom.xml
toolkit-presentation
diff --git a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/BaseItemModel.kt b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/BaseItemModel.kt
index 8f011ba30..9d344df48 100644
--- a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/BaseItemModel.kt
+++ b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/BaseItemModel.kt
@@ -1,7 +1,11 @@
package tech.coner.trailer.toolkit.presentation.model
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import tech.coner.trailer.toolkit.validation.Feedback
import tech.coner.trailer.toolkit.validation.ValidationResult
@@ -27,29 +31,39 @@ abstract class BaseItemModel-
set(value) = mutatePendingItem { value }
private val _pendingItemValidationFlow by lazy {
- MutableStateFlow>(
- ValidationResult(emptyMap())
+ MutableSharedFlow>(
+ replay = 1,
+ onBufferOverflow = BufferOverflow.DROP_OLDEST
)
}
- final override val pendingItemValidationFlow by lazy { _pendingItemValidationFlow.asStateFlow() }
- final override val pendingItemValidation get() = _pendingItemValidationFlow.value
+ final override val pendingItemValidationFlow: Flow> by lazy {
+ _pendingItemValidationFlow
+ }
+ private var _pendingItemValidation: ValidationResult
- = ValidationResult(emptyMap())
+ final override val pendingItemValidation get() = _pendingItemValidation
final override fun mutatePendingItem(forceValidate: Boolean?, mutatePendingItemFn: (ITEM) -> ITEM) {
+ val pendingItemHadFeedback = _pendingItemValidation.feedback.isNotEmpty()
_pendingItemFlow.update { pending -> mutatePendingItemFn(pending) }
- if (forceValidate == true) {
+ if (forceValidate == true || pendingItemHadFeedback) {
validate()
}
}
final override val isPendingItemValid
- get() = _pendingItemValidationFlow.value.isValid
+ get() = _pendingItemValidation.isValid
final override val isPendingItemDirty
get() = item != pendingItem
+ final override val isPendingItemDirtyFlow: Flow by lazy { pendingItemFlow.map { item != it } }
+
final override fun validate(): ValidationResult
- {
return validator(validatorContext, pendingItem)
- .also { _pendingItemValidationFlow.value = it }
+ .also {
+ _pendingItemValidation = it
+ _pendingItemValidationFlow.tryEmit(it)
+ }
}
override fun commit(requireValid: Boolean): CommitOutcome
- {
@@ -63,4 +77,11 @@ abstract class BaseItemModel
-
.also { _itemFlow.value = it }
.let { CommitOutcome.Success(it, validationResult) }
}
+
+ fun reset() {
+ _itemFlow.value = initialItem
+ _pendingItemFlow.value = initialItem
+ _pendingItemValidation = ValidationResult
- (emptyMap())
+ .also { _pendingItemValidationFlow.tryEmit(it) }
+ }
}
\ No newline at end of file
diff --git a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/CommitOutcome.kt b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/CommitOutcome.kt
index 3d86761b4..1763d3462 100644
--- a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/CommitOutcome.kt
+++ b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/CommitOutcome.kt
@@ -18,12 +18,20 @@ sealed class CommitOutcome
- {
override val feedback: ValidationResult
-
) : CommitOutcome
- ()
- fun onSuccess(fn: (Success
- ) -> Unit): CommitOutcome
- {
+ suspend fun onSuccess(fn: suspend (Success
- ) -> Unit): CommitOutcome
- {
if (this is Success
- ) fn(this)
return this
}
- fun onFailure(fn: (Failure
- ) -> Unit): CommitOutcome
- {
+ suspend fun suspendOnSuccessLet(fn: suspend (Success
- ) -> R): R? {
+ return when (this) {
+ is Success
- -> fn(this)
+ else -> null
+ }
+
+ }
+
+ suspend fun onFailure(fn: suspend (Failure
- ) -> Unit): CommitOutcome
- {
if (this is Failure
- ) fn(this)
return this
}
diff --git a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/ItemModel.kt b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/ItemModel.kt
index a3b75ec46..3793cbf59 100644
--- a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/ItemModel.kt
+++ b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/ItemModel.kt
@@ -13,6 +13,7 @@ interface ItemModel
- : Model {
var pendingItem: ITEM
val isPendingItemDirty: Boolean
+ val isPendingItemDirtyFlow: Flow
val pendingItemValidationFlow: Flow>
val pendingItemValidation: ValidationResult
-
diff --git a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/presenter/SecondDraftPresenter.kt b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/presenter/SecondDraftPresenter.kt
index f161a3a88..6f211486b 100644
--- a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/presenter/SecondDraftPresenter.kt
+++ b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/presenter/SecondDraftPresenter.kt
@@ -8,10 +8,8 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import tech.coner.trailer.toolkit.presentation.model.Model
-import tech.coner.trailer.toolkit.presentation.state.State
-abstract class SecondDraftPresenter
- where STATE : State {
+abstract class SecondDraftPresenter {
abstract val initialState: STATE
private val _stateMutex = Mutex()
diff --git a/toolkit/samples/dmvapp/dmvapp-cli/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/cli/command/DriversLicenseApplicationCommand.kt b/toolkit/samples/dmvapp/dmvapp-cli/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/cli/command/DriversLicenseApplicationCommand.kt
index 4f069b78a..548de0cf6 100644
--- a/toolkit/samples/dmvapp/dmvapp-cli/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/cli/command/DriversLicenseApplicationCommand.kt
+++ b/toolkit/samples/dmvapp/dmvapp-cli/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/cli/command/DriversLicenseApplicationCommand.kt
@@ -1,14 +1,19 @@
package tech.coner.trailer.toolkit.sample.dmvapp.cli.command
-import com.github.ajalt.clikt.core.Abort
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.required
import com.github.ajalt.clikt.parameters.types.choice
import com.github.ajalt.clikt.parameters.types.int
+import kotlin.random.Random
+import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import tech.coner.trailer.toolkit.sample.dmvapp.cli.view.DriversLicenseView
import tech.coner.trailer.toolkit.sample.dmvapp.domain.entity.LicenseType
@@ -36,28 +41,38 @@ class DriversLicenseApplicationCommand(
.choice(localization.licenseTypeLabels.associate { it.second.dashify() to it.first })
.required()
- override fun run() {
- val model = presenter.state.model
- model.name = name
- model.age = age
- model.licenseType = licenseType
- model.commit()
- .onFailure {
- it.feedback.echo()
- throw Abort()
- }
- runBlocking { presenter.processApplication() }
- ?.also {
- echo(localization.driversLicenseGranted)
- echo(driversLicenseView(it.driversLicense!!))
+ override fun run(): Unit = runBlocking {
+ presenter.name = name
+ presenter.ageAsInt = age
+ presenter.licenseType = licenseType
+
+ val processing = async {
+ val progress = launch {
+ echo("Processing...", trailingNewline = false)
+ while (isActive) {
+ echo('.', trailingNewline = false)
+ delay(Random.nextInt(100, 1000).milliseconds)
+ }
}
+ presenter.processApplication()
+ .also { progress.cancel() }
+ }
+
+ val outcome = processing.await()
+ echo()
+ if (outcome != null) {
+ echo(localization.driversLicenseGranted)
+ echo(driversLicenseView(outcome.driversLicense!!))
+ } else {
+ presenter.validationResult.echo()
+ }
}
private fun ValidationResult.echo() {
if (feedback.isEmpty()) return
feedback
.map { (_, feedbacks) ->
- feedbacks.joinToString(separator = System.lineSeparator()) { "[${it.severity.name.lowercase()}]: ${localization.label(it)}" }
+ feedbacks.joinToString(separator = System.lineSeparator()) { "[${it.severity.name.lowercase()}]: ${localization.message(it)}" }
}
.forEach { echo(it, err = true) }
}
diff --git a/toolkit/samples/dmvapp/dmvapp-common/src/main/kotlin/tech.coner.trailer.toolkit.sample.dmvapp/domain/entity/DriversLicenseApplication.kt b/toolkit/samples/dmvapp/dmvapp-common/src/main/kotlin/tech.coner.trailer.toolkit.sample.dmvapp/domain/entity/DriversLicenseApplication.kt
index 8b3024ebe..afb0b47ef 100644
--- a/toolkit/samples/dmvapp/dmvapp-common/src/main/kotlin/tech.coner.trailer.toolkit.sample.dmvapp/domain/entity/DriversLicenseApplication.kt
+++ b/toolkit/samples/dmvapp/dmvapp-common/src/main/kotlin/tech.coner.trailer.toolkit.sample.dmvapp/domain/entity/DriversLicenseApplication.kt
@@ -1,16 +1,16 @@
package tech.coner.trailer.toolkit.sample.dmvapp.domain.entity
-import kotlin.reflect.KProperty1
import tech.coner.trailer.toolkit.sample.dmvapp.domain.validation.DriversLicenseApplicationFeedback
+import tech.coner.trailer.toolkit.validation.ValidationResult
data class DriversLicenseApplication(
val name: String,
val age: Int,
- val licenseType: tech.coner.trailer.toolkit.sample.dmvapp.domain.entity.LicenseType,
+ val licenseType: LicenseType,
) {
data class Outcome(
- val driversLicense: tech.coner.trailer.toolkit.sample.dmvapp.domain.entity.DriversLicense?,
- val feedback: Map?, List>
+ val driversLicense: DriversLicense?,
+ val feedback: ValidationResult
)
}
diff --git a/toolkit/samples/dmvapp/dmvapp-common/src/main/kotlin/tech.coner.trailer.toolkit.sample.dmvapp/domain/service/impl/DriversLicenseApplicationServiceImpl.kt b/toolkit/samples/dmvapp/dmvapp-common/src/main/kotlin/tech.coner.trailer.toolkit.sample.dmvapp/domain/service/impl/DriversLicenseApplicationServiceImpl.kt
index 7c2df64a9..864f138c9 100644
--- a/toolkit/samples/dmvapp/dmvapp-common/src/main/kotlin/tech.coner.trailer.toolkit.sample.dmvapp/domain/service/impl/DriversLicenseApplicationServiceImpl.kt
+++ b/toolkit/samples/dmvapp/dmvapp-common/src/main/kotlin/tech.coner.trailer.toolkit.sample.dmvapp/domain/service/impl/DriversLicenseApplicationServiceImpl.kt
@@ -1,5 +1,7 @@
package tech.coner.trailer.toolkit.sample.dmvapp.domain.service.impl
+import kotlin.random.Random
+import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.delay
import tech.coner.trailer.toolkit.sample.dmvapp.domain.entity.DriversLicense
@@ -13,8 +15,9 @@ internal class DriversLicenseApplicationServiceImpl(
) : DriversLicenseApplicationService {
override suspend fun process(application: DriversLicenseApplication): DriversLicenseApplication.Outcome {
+ delay(Random.nextDouble(1000.0, 10000.0).milliseconds)
return clerk(application)
- .also { if (it.isInvalid) delay(3.seconds) }
+ .also { if (it.isInvalid) delay(5.seconds) }
.let {
DriversLicenseApplication.Outcome(
driversLicense = it.whenValid {
@@ -24,7 +27,7 @@ internal class DriversLicenseApplicationServiceImpl(
licenseType = application.licenseType
)
},
- feedback = it.feedback
+ feedback = it
)
}
}
diff --git a/toolkit/samples/dmvapp/dmvapp-common/src/main/kotlin/tech.coner.trailer.toolkit.sample.dmvapp/presentation/localization/EnglishUsLocalization.kt b/toolkit/samples/dmvapp/dmvapp-common/src/main/kotlin/tech.coner.trailer.toolkit.sample.dmvapp/presentation/localization/EnglishUsLocalization.kt
index 6f1b7dc94..9c1ddfbbd 100644
--- a/toolkit/samples/dmvapp/dmvapp-common/src/main/kotlin/tech.coner.trailer.toolkit.sample.dmvapp/presentation/localization/EnglishUsLocalization.kt
+++ b/toolkit/samples/dmvapp/dmvapp-common/src/main/kotlin/tech.coner.trailer.toolkit.sample.dmvapp/presentation/localization/EnglishUsLocalization.kt
@@ -1,5 +1,6 @@
package tech.coner.trailer.toolkit.sample.dmvapp.presentation.localization
+import tech.coner.trailer.toolkit.sample.dmvapp.domain.entity.LicenseType
import tech.coner.trailer.toolkit.sample.dmvapp.domain.entity.LicenseType.FullLicense
import tech.coner.trailer.toolkit.sample.dmvapp.domain.entity.LicenseType.GraduatedLearnerPermit
import tech.coner.trailer.toolkit.sample.dmvapp.domain.entity.LicenseType.LearnerPermit
@@ -21,7 +22,11 @@ class EnglishUsLocalization : Localization {
)
override val licenseTypesByObject = licenseTypeLabels.toMap()
- override fun label(model: DriversLicenseApplicationModelFeedback) = model.label
+ override fun get(licenseType: LicenseType) = licenseTypesByObject[licenseType] ?: ""
+
+ override fun message(feedback: DriversLicenseApplicationModelFeedback) = feedback.label
+
+ override fun message(feedback: DriversLicenseApplicationFeedback) = feedback.label
private val DriversLicenseApplicationModelFeedback.label: String
get() = when (this) {
@@ -62,5 +67,6 @@ class EnglishUsLocalization : Localization {
override val driversLicenseNameField: String get() = "Name"
override val driversLicenseAgeWhenAppliedField: String get() = "Age"
override val driversLicenseLicenseTypeField: String get() = "License Type"
-
+ override val driversLicenseApplicationFormReset: String get() = "Reset"
+ override val driversLicenseApplicationFormApply: String get() = "Apply"
}
\ No newline at end of file
diff --git a/toolkit/samples/dmvapp/dmvapp-common/src/main/kotlin/tech.coner.trailer.toolkit.sample.dmvapp/presentation/localization/Localization.kt b/toolkit/samples/dmvapp/dmvapp-common/src/main/kotlin/tech.coner.trailer.toolkit.sample.dmvapp/presentation/localization/Localization.kt
index 45654db6f..2f6e542be 100644
--- a/toolkit/samples/dmvapp/dmvapp-common/src/main/kotlin/tech.coner.trailer.toolkit.sample.dmvapp/presentation/localization/Localization.kt
+++ b/toolkit/samples/dmvapp/dmvapp-common/src/main/kotlin/tech.coner.trailer.toolkit.sample.dmvapp/presentation/localization/Localization.kt
@@ -1,19 +1,24 @@
package tech.coner.trailer.toolkit.sample.dmvapp.presentation.localization
import tech.coner.trailer.toolkit.sample.dmvapp.domain.entity.LicenseType
+import tech.coner.trailer.toolkit.sample.dmvapp.domain.validation.DriversLicenseApplicationFeedback
import tech.coner.trailer.toolkit.sample.dmvapp.presentation.validation.DriversLicenseApplicationModelFeedback
interface Localization {
- fun label(model: DriversLicenseApplicationModelFeedback): String
+ fun message(feedback: DriversLicenseApplicationModelFeedback): String
val dmvLabel: String
val dmvMotto: String
val licenseTypeLabels: List>
val licenseTypesByObject: Map
+ operator fun get(licenseType: LicenseType): String?
val driversLicenseGranted: String
val driversLicenseHeading: String
val driversLicensePhotoPlaceholder: String
val driversLicenseNameField: String
val driversLicenseAgeWhenAppliedField: String
val driversLicenseLicenseTypeField: String
+ val driversLicenseApplicationFormReset: String
+ val driversLicenseApplicationFormApply: String
+ fun message(feedback: DriversLicenseApplicationFeedback): String
}
\ No newline at end of file
diff --git a/toolkit/samples/dmvapp/dmvapp-common/src/main/kotlin/tech.coner.trailer.toolkit.sample.dmvapp/presentation/model/DriversLicenseApplicationItemModel.kt b/toolkit/samples/dmvapp/dmvapp-common/src/main/kotlin/tech.coner.trailer.toolkit.sample.dmvapp/presentation/model/DriversLicenseApplicationItemModel.kt
index aa10659ff..bf8c9aefe 100644
--- a/toolkit/samples/dmvapp/dmvapp-common/src/main/kotlin/tech.coner.trailer.toolkit.sample.dmvapp/presentation/model/DriversLicenseApplicationItemModel.kt
+++ b/toolkit/samples/dmvapp/dmvapp-common/src/main/kotlin/tech.coner.trailer.toolkit.sample.dmvapp/presentation/model/DriversLicenseApplicationItemModel.kt
@@ -1,6 +1,5 @@
package tech.coner.trailer.toolkit.sample.dmvapp.presentation.model
-import kotlinx.coroutines.flow.map
import tech.coner.trailer.toolkit.presentation.model.BaseItemModel
import tech.coner.trailer.toolkit.sample.dmvapp.domain.entity.LicenseType
import tech.coner.trailer.toolkit.sample.dmvapp.presentation.validation.DriversLicenseApplicationModelFeedback
@@ -13,19 +12,16 @@ class DriversLicenseApplicationItemModel(
override val initialItem: DriversLicenseApplicationModel = DriversLicenseApplicationModel()
override val validatorContext = Unit
- var name: String?
- get() = pendingItem.name
+ var name: String
+ get() = pendingItem.name ?: ""
set(value) = mutatePendingItem { it.copy(name = value) }
- val nameFlow = pendingItemFlow.map { it.name }
var age: Int?
get() = pendingItem.age
set(value) = mutatePendingItem { it.copy(age = value) }
- val ageFlow = pendingItemFlow.map { it.age }
var licenseType: LicenseType?
get() = pendingItem.licenseType
set(value) = mutatePendingItem { it.copy(licenseType = value) }
- val licenseTypeFlow = pendingItemFlow.map { it.licenseType }
}
diff --git a/toolkit/samples/dmvapp/dmvapp-common/src/main/kotlin/tech.coner.trailer.toolkit.sample.dmvapp/presentation/presenter/DriversLicenseApplicationPresenter.kt b/toolkit/samples/dmvapp/dmvapp-common/src/main/kotlin/tech.coner.trailer.toolkit.sample.dmvapp/presentation/presenter/DriversLicenseApplicationPresenter.kt
index d5a4d2351..a9ff3a4aa 100644
--- a/toolkit/samples/dmvapp/dmvapp-common/src/main/kotlin/tech.coner.trailer.toolkit.sample.dmvapp/presentation/presenter/DriversLicenseApplicationPresenter.kt
+++ b/toolkit/samples/dmvapp/dmvapp-common/src/main/kotlin/tech.coner.trailer.toolkit.sample.dmvapp/presentation/presenter/DriversLicenseApplicationPresenter.kt
@@ -1,14 +1,21 @@
package tech.coner.trailer.toolkit.sample.dmvapp.presentation.presenter
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.zip
import org.kodein.di.DI
import org.kodein.di.DIAware
import org.kodein.di.instance
import tech.coner.trailer.toolkit.presentation.presenter.SecondDraftPresenter
import tech.coner.trailer.toolkit.sample.dmvapp.domain.entity.DriversLicenseApplication
+import tech.coner.trailer.toolkit.sample.dmvapp.domain.entity.LicenseType
import tech.coner.trailer.toolkit.sample.dmvapp.domain.service.DriversLicenseApplicationService
import tech.coner.trailer.toolkit.sample.dmvapp.presentation.adapter.toDomainEntity
+import tech.coner.trailer.toolkit.sample.dmvapp.presentation.localization.Localization
import tech.coner.trailer.toolkit.sample.dmvapp.presentation.model.DriversLicenseApplicationItemModel
+import tech.coner.trailer.toolkit.sample.dmvapp.presentation.model.DriversLicenseApplicationModel
import tech.coner.trailer.toolkit.sample.dmvapp.presentation.state.DriversLicenseApplicationState
+import tech.coner.trailer.toolkit.sample.dmvapp.presentation.validation.DriversLicenseApplicationModelFeedback
import tech.coner.trailer.toolkit.sample.dmvapp.presentation.validation.DriversLicenseApplicationModelValidator
class DriversLicenseApplicationPresenter(
@@ -18,17 +25,67 @@ class DriversLicenseApplicationPresenter(
private val service: DriversLicenseApplicationService by instance()
private val validator: DriversLicenseApplicationModelValidator by instance()
+ private val localization: Localization by instance()
- override val initialState = initialState
- ?: DriversLicenseApplicationState(
- model = DriversLicenseApplicationItemModel(
- validator = validator
- )
- )
+ override val initialState = initialState ?: DriversLicenseApplicationState()
+
+ private val model: DriversLicenseApplicationItemModel = DriversLicenseApplicationItemModel(
+ validator = validator
+ )
suspend fun processApplication(): DriversLicenseApplication.Outcome? {
- return state.model.item.toDomainEntity()
- ?.let { service.process(it) }
- ?.also { outcome -> update { it.copy(outcome = outcome) } }
+ // prevent double entry
+ if (state.processing) return null
+
+ update { it.copy(processing = true) }
+ return model.commit()
+ .onFailure { update { it.copy(processing = false) } }
+ .suspendOnSuccessLet { success ->
+ success.item.toDomainEntity()
+ ?.let { service.process(it) }
+ ?.also { outcome -> update { it.copy(outcome = outcome, processing = false) } }
+ }
}
+
+ suspend fun reset() {
+ model.reset()
+ update { DriversLicenseApplicationState() }
+ }
+
+ val validationResult get() = model.pendingItemValidation
+ val validationResultFlow get() = model.pendingItemValidationFlow
+
+ val nameFlow: Flow = model.pendingItemFlow.map { it.name ?: "" }
+ var name: String
+ get() = model.name
+ set(value) { model.name = value }
+ val nameFeedback: Flow
> = model.pendingItemValidationFlow
+ .map {
+ it.feedback[DriversLicenseApplicationModel::name] ?: emptyList()
+ }
+
+ val ageFlow: Flow = model.pendingItemFlow.map { it.age?.toString() ?: "" }
+ var age: String?
+ get() = model.age?.toString() ?: ""
+ set(value) { model.age = value?.toIntOrNull() }
+ var ageAsInt: Int?
+ get() = model.age
+ set(value) { model.age = value }
+ val ageFeedback = model.pendingItemValidationFlow
+ .map { it.feedback[DriversLicenseApplicationModel::age] ?: emptyList() }
+
+ val licenseTypeFlow: Flow = model.pendingItemFlow.map { it.licenseType }
+ var licenseType: LicenseType?
+ get() = model.licenseType
+ set(value) { model.licenseType = value }
+ val licenseTypeFeedback = model.pendingItemValidationFlow
+ .map { it.feedback[DriversLicenseApplicationModel::licenseType] ?: emptyList() }
+
+ val processingApplicationFlow: Flow = stateFlow.map { it.processing }
+ val fieldsEditableFlow: Flow = stateFlow.map { !it.processing && it.outcome == null }
+ val resetButtonEnabledFlow: Flow = model.isPendingItemDirtyFlow
+ .zip(processingApplicationFlow) { dirty, processing ->
+ dirty && !processing
+ }
+ val applyButtonEnabled: Flow = stateFlow.map { !it.processing && it.outcome == null }
}
\ No newline at end of file
diff --git a/toolkit/samples/dmvapp/dmvapp-common/src/main/kotlin/tech.coner.trailer.toolkit.sample.dmvapp/presentation/state/DriversLicenseApplicationState.kt b/toolkit/samples/dmvapp/dmvapp-common/src/main/kotlin/tech.coner.trailer.toolkit.sample.dmvapp/presentation/state/DriversLicenseApplicationState.kt
index fe5a987ef..99fe27455 100644
--- a/toolkit/samples/dmvapp/dmvapp-common/src/main/kotlin/tech.coner.trailer.toolkit.sample.dmvapp/presentation/state/DriversLicenseApplicationState.kt
+++ b/toolkit/samples/dmvapp/dmvapp-common/src/main/kotlin/tech.coner.trailer.toolkit.sample.dmvapp/presentation/state/DriversLicenseApplicationState.kt
@@ -2,9 +2,8 @@ package tech.coner.trailer.toolkit.sample.dmvapp.presentation.state
import tech.coner.trailer.toolkit.presentation.state.State
import tech.coner.trailer.toolkit.sample.dmvapp.domain.entity.DriversLicenseApplication
-import tech.coner.trailer.toolkit.sample.dmvapp.presentation.model.DriversLicenseApplicationItemModel
data class DriversLicenseApplicationState(
- val model: DriversLicenseApplicationItemModel,
+ val processing: Boolean = false,
val outcome: DriversLicenseApplication.Outcome? = null
) : State
diff --git a/toolkit/samples/dmvapp/dmvapp-common/src/test/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/domain/service/impl/DriversLicenseApplicationServiceImplTest.kt b/toolkit/samples/dmvapp/dmvapp-common/src/test/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/domain/service/impl/DriversLicenseApplicationServiceImplTest.kt
index ea0e662f3..97e56559e 100644
--- a/toolkit/samples/dmvapp/dmvapp-common/src/test/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/domain/service/impl/DriversLicenseApplicationServiceImplTest.kt
+++ b/toolkit/samples/dmvapp/dmvapp-common/src/test/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/domain/service/impl/DriversLicenseApplicationServiceImplTest.kt
@@ -11,6 +11,7 @@ import tech.coner.trailer.toolkit.sample.dmvapp.domain.entity.LicenseType.FullLi
import tech.coner.trailer.toolkit.sample.dmvapp.domain.service.DriversLicenseApplicationService
import tech.coner.trailer.toolkit.sample.dmvapp.domain.validation.DriversLicenseApplicationFeedback.NameMustNotBeBlank
import tech.coner.trailer.toolkit.sample.dmvapp.domain.validation.DriversLicenseClerk
+import tech.coner.trailer.toolkit.validation.ValidationResult
class DriversLicenseApplicationServiceImplTest {
@@ -34,7 +35,7 @@ class DriversLicenseApplicationServiceImplTest {
ageWhenApplied = 18,
licenseType = FullLicense
),
- feedback = emptyMap()
+ feedback = ValidationResult(emptyMap())
)
),
INVALID(
@@ -45,8 +46,10 @@ class DriversLicenseApplicationServiceImplTest {
),
expected = DriversLicenseApplication.Outcome(
driversLicense = null,
- feedback = mapOf(
- DriversLicenseApplication::name to listOf(NameMustNotBeBlank)
+ feedback = ValidationResult(
+ mapOf(
+ DriversLicenseApplication::name to listOf(NameMustNotBeBlank)
+ )
)
)
)
diff --git a/toolkit/samples/dmvapp/dmvapp-common/src/test/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/presentation/presenter/DriversLicenseApplicationPresenterTest.kt b/toolkit/samples/dmvapp/dmvapp-common/src/test/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/presentation/presenter/DriversLicenseApplicationPresenterTest.kt
index b502bf9f7..7e62ac9ea 100644
--- a/toolkit/samples/dmvapp/dmvapp-common/src/test/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/presentation/presenter/DriversLicenseApplicationPresenterTest.kt
+++ b/toolkit/samples/dmvapp/dmvapp-common/src/test/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/presentation/presenter/DriversLicenseApplicationPresenterTest.kt
@@ -2,14 +2,11 @@ package tech.coner.trailer.toolkit.sample.dmvapp.presentation.presenter
import assertk.all
import assertk.assertThat
-import assertk.assertions.isEmpty
import assertk.assertions.isEqualTo
-import assertk.assertions.isFalse
import assertk.assertions.isNotNull
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
-import org.junit.jupiter.api.fail
import tech.coner.trailer.toolkit.sample.dmvapp.domain.entity.DriversLicense
import tech.coner.trailer.toolkit.sample.dmvapp.domain.entity.LicenseType.FullLicense
import tech.coner.trailer.toolkit.sample.dmvapp.domain.entity.driversLicense
@@ -20,6 +17,7 @@ import tech.coner.trailer.toolkit.sample.dmvapp.presentation.validation.DriversL
import tech.coner.trailer.toolkit.sample.dmvapp.presentation.validation.DriversLicenseApplicationModelFeedback.NameIsRequired
import tech.coner.trailer.toolkit.sample.dmvapp.testDi
import tech.coner.trailer.toolkit.validation.testsupport.feedback
+import tech.coner.trailer.toolkit.validation.testsupport.isInvalid
import tech.coner.trailer.toolkit.validation.testsupport.isValid
class DriversLicenseApplicationPresenterTest {
@@ -35,47 +33,42 @@ class DriversLicenseApplicationPresenterTest {
ageWhenApplied = 42,
licenseType = FullLicense
)
- with(presenter.state.model) {
+ with(presenter) {
name = license.name
- age = license.ageWhenApplied
+ ageAsInt = license.ageWhenApplied
licenseType = license.licenseType
- commit()
- .onSuccess {
- runBlocking {
- presenter.processApplication()
- }
- }
+ processApplication()
}
assertThat(presenter.state.outcome)
.isNotNull()
.all {
driversLicense().isEqualTo(license)
- feedback().isEmpty()
+ feedback().isValid()
}
}
@Test
fun itShouldHandleInvalidApplication() = runTest {
- with(presenter.state.model) {
+ with(presenter) {
// model's initial values aren't valid
- commit()
- .onSuccess {
- fail("unexpected: commit invoked successFn")
- }
+ processApplication()
+ runCurrent()
}
- assertThat(presenter.state.model.pendingItemValidation).all {
- isValid().isFalse()
- feedback().isEqualTo(
- mapOf(
- DriversLicenseApplicationModel::name to listOf(NameIsRequired),
- DriversLicenseApplicationModel::age to listOf(AgeIsRequired),
- DriversLicenseApplicationModel::licenseType to listOf(LicenseTypeIsRequired)
+ assertThat(presenter.validationResult)
+ .isNotNull()
+ .all {
+ isInvalid()
+ feedback().isEqualTo(
+ mapOf(
+ DriversLicenseApplicationModel::name to listOf(NameIsRequired),
+ DriversLicenseApplicationModel::age to listOf(AgeIsRequired),
+ DriversLicenseApplicationModel::licenseType to listOf(LicenseTypeIsRequired)
+ )
)
- )
}
}
}
\ No newline at end of file
diff --git a/toolkit/samples/dmvapp/dmvapp-gui/.run/desktop.run.xml b/toolkit/samples/dmvapp/dmvapp-gui/.run/desktop.run.xml
new file mode 100644
index 000000000..4f4322471
--- /dev/null
+++ b/toolkit/samples/dmvapp/dmvapp-gui/.run/desktop.run.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+
\ No newline at end of file
diff --git a/toolkit/samples/dmvapp/dmvapp-gui/build.gradle.kts b/toolkit/samples/dmvapp/dmvapp-gui/build.gradle.kts
new file mode 100644
index 000000000..ad0c14c20
--- /dev/null
+++ b/toolkit/samples/dmvapp/dmvapp-gui/build.gradle.kts
@@ -0,0 +1,71 @@
+
+import java.nio.file.Paths
+import java.util.Properties
+import kotlin.io.path.inputStream
+import org.jetbrains.compose.desktop.application.dsl.TargetFormat
+
+plugins {
+ kotlin("jvm")
+ id("org.jetbrains.compose")
+ id("org.jetbrains.kotlin.plugin.compose")
+}
+
+val mavenProperties: Properties = file(Paths.get("target", "project.properties"))
+ .inputStream()
+ .buffered()
+ .use {
+ Properties()
+ .apply { load(it) }
+ }
+
+version = mavenProperties["coner-trailer.version"]
+ ?: throw IllegalStateException("coner-trailer.version property missing from mavenProperties")
+
+repositories {
+ maven("target/dependencies")
+ mavenCentral()
+ maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
+ google()
+}
+
+dependencies {
+ implementation("tech.coner.trailer:toolkit-sample-dmvapp-common:$version")
+
+ // Note, if you develop a library, you should use compose.desktop.common.
+ // compose.desktop.currentOs should be used in launcher-sourceSet
+ // (in a separate module for demo project and in testMain).
+ // With compose.desktop.common you will also lose @Preview functionality
+ implementation(compose.desktop.currentOs)
+ implementation(compose.material3)
+ implementation("tech.annexflow.compose:constraintlayout-compose-multiplatform:0.4.0")
+
+ implementation("org.kodein.di:kodein-di-framework-compose:${mavenProperties["kodein-di.version"]}")
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:${mavenProperties["kotlinx-coroutines.version"]}")
+
+ testImplementation(kotlin("test"))
+ @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
+ testImplementation(compose.uiTest)
+ testImplementation(compose.desktop.currentOs)
+}
+
+kotlin {
+ compilerOptions {
+ optIn.addAll(
+ "androidx.compose.foundation.ExperimentalFoundationApi",
+ "androidx.compose.material3.ExperimentalMaterial3Api",
+ "androidx.compose.ui.test.ExperimentalTestApi"
+ )
+ }
+}
+
+compose.desktop {
+ application {
+ mainClass = "tech.coner.trailer.toolkit.sample.dmvapp.gui.DmvAppGuiKt"
+
+ nativeDistributions {
+ targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
+ packageName = "toolkit-sample-dmvapp-gui"
+ packageVersion = "1.0.0"
+ }
+ }
+}
diff --git a/toolkit/samples/dmvapp/dmvapp-gui/gradle.properties b/toolkit/samples/dmvapp/dmvapp-gui/gradle.properties
new file mode 100644
index 000000000..1b291c2c4
--- /dev/null
+++ b/toolkit/samples/dmvapp/dmvapp-gui/gradle.properties
@@ -0,0 +1,4 @@
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+kotlin.code.style=official
+kotlin.version=2.0.0
+compose.version=1.6.10
diff --git a/toolkit/samples/dmvapp/dmvapp-gui/gradle/wrapper/gradle-wrapper.jar b/toolkit/samples/dmvapp/dmvapp-gui/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..249e5832f
Binary files /dev/null and b/toolkit/samples/dmvapp/dmvapp-gui/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/toolkit/samples/dmvapp/dmvapp-gui/gradle/wrapper/gradle-wrapper.properties b/toolkit/samples/dmvapp/dmvapp-gui/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..48c0a02ca
--- /dev/null
+++ b/toolkit/samples/dmvapp/dmvapp-gui/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/toolkit/samples/dmvapp/dmvapp-gui/gradlew b/toolkit/samples/dmvapp/dmvapp-gui/gradlew
new file mode 100755
index 000000000..1b6c78733
--- /dev/null
+++ b/toolkit/samples/dmvapp/dmvapp-gui/gradlew
@@ -0,0 +1,234 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+
+APP_NAME="Gradle"
+APP_BASE_NAME=${0##*/}
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+# Collect all arguments for the java command;
+# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+# shell script including quotes and variable substitutions, so put them in
+# double quotes to make sure that they get re-expanded; and
+# * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/toolkit/samples/dmvapp/dmvapp-gui/gradlew.bat b/toolkit/samples/dmvapp/dmvapp-gui/gradlew.bat
new file mode 100644
index 000000000..ac1b06f93
--- /dev/null
+++ b/toolkit/samples/dmvapp/dmvapp-gui/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/toolkit/samples/dmvapp/dmvapp-gui/pom.xml b/toolkit/samples/dmvapp/dmvapp-gui/pom.xml
new file mode 100644
index 000000000..ee6229992
--- /dev/null
+++ b/toolkit/samples/dmvapp/dmvapp-gui/pom.xml
@@ -0,0 +1,138 @@
+
+
+ 4.0.0
+
+ tech.coner.trailer
+ parent
+ 0.1.0-SNAPSHOT
+ ../../../../pom.xml
+
+
+ toolkit-sample-dmvapp-gui
+
+
+ ./gradlew
+
+
+
+
+ tech.coner.trailer
+ toolkit-sample-dmvapp-common
+ 0.1.0-SNAPSHOT
+
+
+
+
+
+
+ org.codehaus.mojo
+ properties-maven-plugin
+
+
+ generate-resources
+
+ write-project-properties
+
+
+ ${project.build.directory}/project.properties
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+
+
+ copy-dependencies
+ generate-resources
+
+ copy-dependencies
+
+ false
+
+ true
+ true
+ ${project.build.directory}/dependencies
+ true
+ true
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+
+
+ gradle-build
+ prepare-package
+
+ ${gradle.executable}
+
+ ${gradle.argument.daemon}
+ build
+ -S
+
+
+
+ exec
+
+
+
+
+
+ maven-resources-plugin
+
+
+ copy-gradle-jars
+ package
+
+ copy-resources
+
+
+
+ ${basedir}/target
+
+
+ build/libs/
+
+ ${project.artifactId}-${project.version}.jar
+ ${project.artifactId}-${project.version}-javadoc.jar
+ ${project.artifactId}-${project.version}-sources.jar
+
+
+
+
+
+
+
+
+
+
+
+
+ gradle-default
+
+ true
+
+
+ --daemon
+
+
+
+ gradle-windows
+
+
+ windows
+
+
+
+
+ --no-daemon
+
+
+
+
diff --git a/toolkit/samples/dmvapp/dmvapp-gui/settings.gradle.kts b/toolkit/samples/dmvapp/dmvapp-gui/settings.gradle.kts
new file mode 100644
index 000000000..2956757d3
--- /dev/null
+++ b/toolkit/samples/dmvapp/dmvapp-gui/settings.gradle.kts
@@ -0,0 +1,16 @@
+pluginManagement {
+ repositories {
+ maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
+ google()
+ gradlePluginPortal()
+ mavenCentral()
+ }
+
+ plugins {
+ kotlin("jvm").version(extra["kotlin.version"] as String)
+ id("org.jetbrains.compose").version(extra["compose.version"] as String)
+ id("org.jetbrains.kotlin.plugin.compose").version(extra["kotlin.version"] as String)
+ }
+}
+
+rootProject.name = "toolkit-sample-dmvapp-gui"
diff --git a/toolkit/samples/dmvapp/dmvapp-gui/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/gui/DmvAppGui.kt b/toolkit/samples/dmvapp/dmvapp-gui/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/gui/DmvAppGui.kt
new file mode 100644
index 000000000..91df132b6
--- /dev/null
+++ b/toolkit/samples/dmvapp/dmvapp-gui/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/gui/DmvAppGui.kt
@@ -0,0 +1,28 @@
+package tech.coner.trailer.toolkit.sample.dmvapp.gui
+
+import androidx.compose.ui.window.application
+import org.kodein.di.DI
+import org.kodein.di.compose.withDI
+import tech.coner.trailer.toolkit.sample.dmvapp.domain.service.impl.domainServiceModule
+import tech.coner.trailer.toolkit.sample.dmvapp.domain.validation.domainValidationModule
+import tech.coner.trailer.toolkit.sample.dmvapp.gui.composable.DmvAppWindow
+import tech.coner.trailer.toolkit.sample.dmvapp.presentation.localization.presentationLocalizationModule
+import tech.coner.trailer.toolkit.sample.dmvapp.presentation.presenter.presentationPresenterModule
+import tech.coner.trailer.toolkit.sample.dmvapp.presentation.validation.presentationValidationModule
+
+fun main() = application {
+ withDI(di) {
+ DmvAppWindow()
+ }
+}
+
+
+val di = DI {
+ importAll(
+ domainServiceModule,
+ domainValidationModule,
+ presentationLocalizationModule,
+ presentationPresenterModule,
+ presentationValidationModule
+ )
+}
diff --git a/toolkit/samples/dmvapp/dmvapp-gui/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/gui/composable/DmvAppContent.kt b/toolkit/samples/dmvapp/dmvapp-gui/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/gui/composable/DmvAppContent.kt
new file mode 100644
index 000000000..54ebf099d
--- /dev/null
+++ b/toolkit/samples/dmvapp/dmvapp-gui/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/gui/composable/DmvAppContent.kt
@@ -0,0 +1,16 @@
+package tech.coner.trailer.toolkit.sample.dmvapp.gui.composable
+
+import androidx.compose.desktop.ui.tooling.preview.Preview
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.Composable
+
+@Composable
+@Preview
+fun DmvAppContent() {
+ MaterialTheme {
+ Scaffold {
+ DriversLicenseApplicationFormScreen()
+ }
+ }
+}
diff --git a/toolkit/samples/dmvapp/dmvapp-gui/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/gui/composable/DmvAppWindow.kt b/toolkit/samples/dmvapp/dmvapp-gui/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/gui/composable/DmvAppWindow.kt
new file mode 100644
index 000000000..19b6755ed
--- /dev/null
+++ b/toolkit/samples/dmvapp/dmvapp-gui/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/gui/composable/DmvAppWindow.kt
@@ -0,0 +1,19 @@
+package tech.coner.trailer.toolkit.sample.dmvapp.gui.composable
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.window.ApplicationScope
+import androidx.compose.ui.window.Window
+import org.kodein.di.compose.rememberDI
+import org.kodein.di.instance
+import tech.coner.trailer.toolkit.sample.dmvapp.presentation.localization.Localization
+
+@Composable
+fun ApplicationScope.DmvAppWindow() {
+ val localization: Localization by rememberDI { instance() }
+ Window(
+ title = localization.dmvLabel,
+ onCloseRequest = ::exitApplication
+ ) {
+ DmvAppContent()
+ }
+}
\ No newline at end of file
diff --git a/toolkit/samples/dmvapp/dmvapp-gui/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/gui/composable/DriversLicense.kt b/toolkit/samples/dmvapp/dmvapp-gui/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/gui/composable/DriversLicense.kt
new file mode 100644
index 000000000..3b8c5e650
--- /dev/null
+++ b/toolkit/samples/dmvapp/dmvapp-gui/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/gui/composable/DriversLicense.kt
@@ -0,0 +1,154 @@
+package tech.coner.trailer.toolkit.sample.dmvapp.gui.composable
+
+import androidx.compose.desktop.ui.tooling.preview.Preview
+import androidx.compose.foundation.layout.Box
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.window.Window
+import androidx.constraintlayout.compose.ConstraintLayout
+import org.kodein.di.compose.rememberDI
+import org.kodein.di.instance
+import tech.coner.trailer.toolkit.sample.dmvapp.domain.entity.DriversLicense
+import tech.coner.trailer.toolkit.sample.dmvapp.domain.entity.LicenseType
+import tech.coner.trailer.toolkit.sample.dmvapp.presentation.localization.EnglishUsLocalization
+import tech.coner.trailer.toolkit.sample.dmvapp.presentation.localization.Localization
+
+@Composable
+fun DriversLicenseWindow(
+ driversLicense: DriversLicense,
+ onCloseRequest: () -> Unit
+) {
+ Window(
+ onCloseRequest = onCloseRequest
+ ) {
+ DriversLicenseScreen(driversLicense)
+ }
+}
+
+@Composable
+fun DriversLicenseScreen(
+ driversLicense: DriversLicense
+) {
+ val localization: Localization by rememberDI { instance() }
+ DriversLicenseContent(
+ driversLicense = driversLicense,
+ localization = localization
+ )
+}
+
+@Composable
+private fun DriversLicenseContent(
+ driversLicense: DriversLicense,
+ localization: Localization
+) {
+ ConstraintLayout {
+ val (dmv, heading, dmvMotto, photoBox, nameField, name, ageField, age, typeField, type) = createRefs()
+ val fieldEndBarrier = createEndBarrier(nameField, ageField, typeField)
+ Text(
+ text = localization.dmvLabel,
+ modifier = Modifier
+ .constrainAs(dmv) {
+ start.linkTo(parent.start)
+ end.linkTo(fieldEndBarrier)
+ top.linkTo(parent.top)
+ }
+ )
+ Text(
+ text = localization.driversLicenseHeading,
+ textAlign = TextAlign.End,
+ modifier = Modifier
+ .constrainAs(heading) {
+ start.linkTo(fieldEndBarrier)
+ end.linkTo(parent.end)
+ top.linkTo(parent.top)
+ }
+ )
+ Text(
+ text = localization.dmvMotto,
+ modifier = Modifier
+ .constrainAs(dmvMotto) {
+ start.linkTo(parent.start)
+ top.linkTo(dmv.bottom)
+ },
+ )
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier
+ .constrainAs(photoBox) {
+ start.linkTo(parent.start)
+ bottom.linkTo(parent.bottom)
+ }
+ ) {
+ Text(
+ text = localization.driversLicensePhotoPlaceholder
+ )
+ }
+ Text(
+ text = localization.driversLicenseNameField,
+ modifier = Modifier
+ .constrainAs(nameField) {
+ start.linkTo(photoBox.end)
+ top.linkTo(dmvMotto.bottom)
+ bottom.linkTo(name.bottom)
+ }
+ )
+ Text(
+ text = driversLicense.name,
+ modifier = Modifier
+ .constrainAs(name) {
+ start.linkTo(fieldEndBarrier)
+ top.linkTo(nameField.top)
+ }
+ )
+ Text(
+ text = localization.driversLicenseAgeWhenAppliedField,
+ modifier = Modifier
+ .constrainAs(ageField) {
+ start.linkTo(photoBox.end)
+ top.linkTo(nameField.bottom)
+ bottom.linkTo(age.bottom)
+ }
+ )
+ Text(
+ text = driversLicense.ageWhenApplied.toString(),
+ modifier = Modifier
+ .constrainAs(age) {
+ start.linkTo(fieldEndBarrier)
+ top.linkTo(ageField.top)
+ }
+ )
+ Text(
+ text = localization.driversLicenseLicenseTypeField,
+ modifier = Modifier
+ .constrainAs(typeField) {
+ start.linkTo(photoBox.end)
+ top.linkTo(ageField.bottom)
+ bottom.linkTo(type.bottom)
+ }
+ )
+ Text(
+ text = localization[driversLicense.licenseType] ?: "",
+ modifier = Modifier
+ .constrainAs(type) {
+ start.linkTo(fieldEndBarrier)
+ top.linkTo(typeField.top)
+ }
+ )
+ }
+}
+
+@Composable
+@Preview
+private fun DriversLicensePreview() {
+ DriversLicenseContent(
+ driversLicense = DriversLicense(
+ name = "John Doe",
+ ageWhenApplied = 18,
+ licenseType = LicenseType.FullLicense
+ ),
+ localization = EnglishUsLocalization()
+ )
+}
\ No newline at end of file
diff --git a/toolkit/samples/dmvapp/dmvapp-gui/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/gui/composable/DriversLicenseApplicationForm.kt b/toolkit/samples/dmvapp/dmvapp-gui/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/gui/composable/DriversLicenseApplicationForm.kt
new file mode 100644
index 000000000..7e8e55a5d
--- /dev/null
+++ b/toolkit/samples/dmvapp/dmvapp-gui/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/gui/composable/DriversLicenseApplicationForm.kt
@@ -0,0 +1,323 @@
+
+package tech.coner.trailer.toolkit.sample.dmvapp.gui.composable
+
+import androidx.compose.desktop.ui.tooling.preview.Preview
+import androidx.compose.foundation.layout.*
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.input.key.key
+import androidx.compose.ui.input.key.onKeyEvent
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+import org.kodein.di.compose.localDI
+import org.kodein.di.instance
+import tech.coner.trailer.toolkit.sample.dmvapp.domain.entity.LicenseType
+import tech.coner.trailer.toolkit.sample.dmvapp.gui.util.FocusFirstFocusRequester
+import tech.coner.trailer.toolkit.sample.dmvapp.gui.util.FocusFirstFocusRequester.Companion.FocusFirstFocusRequester
+import tech.coner.trailer.toolkit.sample.dmvapp.gui.util.focusOnError
+import tech.coner.trailer.toolkit.sample.dmvapp.presentation.localization.EnglishUsLocalization
+import tech.coner.trailer.toolkit.sample.dmvapp.presentation.localization.Localization
+import tech.coner.trailer.toolkit.sample.dmvapp.presentation.model.DriversLicenseApplicationModel
+import tech.coner.trailer.toolkit.sample.dmvapp.presentation.presenter.DriversLicenseApplicationPresenter
+import tech.coner.trailer.toolkit.sample.dmvapp.presentation.validation.DriversLicenseApplicationModelFeedback
+import tech.coner.trailer.toolkit.validation.Severity
+import tech.coner.trailer.toolkit.validation.isInvalid
+
+@Composable
+fun DriversLicenseApplicationFormScreen() {
+ val di = localDI()
+ val localization: Localization by di.instance()
+ val presenter: DriversLicenseApplicationPresenter by di.instance()
+ val coroutineScope = rememberCoroutineScope { Dispatchers.Main.immediate + CoroutineName("DriversLicenseApplicationFormScreen") }
+
+ with(coroutineScope) {
+ val name = remember { mutableStateOf("") }
+ val age = remember { mutableStateOf("") }
+ val performReset: () -> Unit = {
+ launch {
+ name.value = ""
+ age.value = ""
+ presenter.reset()
+ }
+ }
+
+ val driversLicense = presenter.stateFlow.map { it.outcome?.driversLicense }.collectAsState(null).value
+ if (driversLicense != null) {
+ DriversLicenseWindow(
+ driversLicense = driversLicense,
+ onCloseRequest = performReset
+ )
+ }
+
+ val focusFirstError = remember {
+ FocusFirstFocusRequester(
+ properties = listOf(
+ DriversLicenseApplicationModel::name,
+ DriversLicenseApplicationModel::age,
+ DriversLicenseApplicationModel::licenseType
+ )
+ )
+ .also { focusRequester ->
+ presenter.validationResultFlow
+ .onEach { validationResult ->
+ validationResult.feedback.entries
+ .firstOrNull { (_, feedbacks) ->
+ feedbacks.any {
+ it.severity == Severity.Error
+ }
+ }
+ ?.key
+ ?.also { property -> focusRequester.requestFocus(property) }
+ }
+ .launchIn(this)
+ }
+ }
+
+ DriversLicenseApplicationFormContent(
+ fieldsEditable = presenter.fieldsEditableFlow.collectAsState(true).value,
+ name = name,
+ onNameChange = { presenter.name = it },
+ nameFeedback = presenter.nameFeedback.collectAsState(emptyList()).value,
+ age = age,
+ onAgeChange = { presenter.age = it },
+ ageFeedback = presenter.ageFeedback.collectAsState(emptyList()).value,
+ licenseType = presenter.licenseTypeFlow.collectAsState(null).value,
+ onLicenseTypeChange = { presenter.licenseType = it },
+ licenseTypeFeedback = presenter.licenseTypeFeedback.collectAsState(emptyList()).value,
+ performReset = performReset,
+ resetButtonEnabled = presenter.resetButtonEnabledFlow.collectAsState(false).value,
+ applyButtonEnabled = presenter.applyButtonEnabled.collectAsState(true).value,
+ onApplyButtonClicked = { launch { presenter.processApplication() } },
+ processingApplication = presenter.processingApplicationFlow.collectAsState(false).value,
+ focusFirstFocusRequester = focusFirstError,
+ localization = localization
+ )
+ }
+}
+
+@Composable
+fun DriversLicenseApplicationFormContent(
+ fieldsEditable: Boolean,
+ name: MutableState,
+ onNameChange: (String) -> Unit,
+ nameFeedback: List,
+ age: MutableState,
+ onAgeChange: (String) -> Unit,
+ ageFeedback: List,
+ licenseType: LicenseType?,
+ onLicenseTypeChange: (LicenseType) -> Unit,
+ licenseTypeFeedback: List,
+ performReset: () -> Unit,
+ resetButtonEnabled: Boolean,
+ applyButtonEnabled: Boolean,
+ onApplyButtonClicked: () -> Unit,
+ processingApplication: Boolean,
+ focusFirstFocusRequester: FocusFirstFocusRequester,
+ localization: Localization
+) {
+
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier
+ .fillMaxSize()
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier
+ .width(400.dp)
+ ) {
+ Row {
+ OutlinedTextField(
+ value = name.value,
+ label = {
+ Text(
+ text = localization.driversLicenseNameField,
+ modifier = Modifier
+ .testTag("nameField")
+ )
+ },
+ singleLine = true,
+ onValueChange = {
+ name.value = it
+ onNameChange(it)
+ },
+ isError = nameFeedback.isInvalid,
+ supportingText = validationFeedbackSupportingText(nameFeedback, localization),
+ readOnly = !fieldsEditable,
+ modifier = Modifier
+ .fillMaxWidth()
+ .focusOnError(focusFirstFocusRequester, DriversLicenseApplicationModel::name)
+ .testTag("name")
+ )
+ }
+ Row {
+ OutlinedTextField(
+ value = age.value,
+ label = {
+ Text(
+ text = localization.driversLicenseAgeWhenAppliedField,
+ modifier = Modifier
+ .testTag("ageField")
+ )
+ },
+ singleLine = true,
+ onValueChange = {
+ if (it.isEmpty() || it.all { char -> char.isDigit() }) {
+ age.value = it
+ onAgeChange(it)
+ }
+ },
+ isError = ageFeedback.isInvalid,
+ supportingText = validationFeedbackSupportingText(ageFeedback, localization),
+ readOnly = !fieldsEditable,
+ modifier = Modifier
+ .fillMaxWidth()
+ .focusOnError(focusFirstFocusRequester, DriversLicenseApplicationModel::age)
+ .testTag("age")
+ )
+ }
+ Row {
+ var expanded by remember { mutableStateOf(false) }
+ val guardedMutateExpanded: (Boolean) -> Unit = {
+ if (fieldsEditable || !it) expanded = it
+ }
+ ExposedDropdownMenuBox(
+ expanded = expanded,
+ onExpandedChange = guardedMutateExpanded
+ ) {
+ OutlinedTextField(
+ value = licenseType?.let { localization[it] } ?: "",
+ label = {
+ Text(
+ text = localization.driversLicenseLicenseTypeField,
+ modifier = Modifier
+ .testTag("licenseTypeField")
+ )
+ },
+ readOnly = true,
+ onValueChange = { },
+ isError = licenseTypeFeedback.isInvalid,
+ supportingText = validationFeedbackSupportingText(licenseTypeFeedback, localization),
+ trailingIcon = {
+ ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
+ },
+ colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
+ modifier = Modifier
+ .fillMaxWidth()
+ .menuAnchor()
+ .onKeyEvent {
+ when {
+ it.key == Key.DirectionDown -> { guardedMutateExpanded(true); true }
+ else -> false
+ }
+ }
+ .focusOnError(focusFirstFocusRequester, DriversLicenseApplicationModel::licenseType)
+ .testTag("licenseType")
+ )
+ ExposedDropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { guardedMutateExpanded(false) },
+ modifier = Modifier
+ .testTag("licenseTypeDropdownMenu")
+ ) {
+ localization.licenseTypeLabels.forEachIndexed { index, pair ->
+ DropdownMenuItem(
+ text = { Text(pair.second) },
+ onClick = {
+ guardedMutateExpanded(false)
+ onLicenseTypeChange(pair.first)
+ },
+ modifier = Modifier
+ .testTag("licenseTypeDropdownMenuItem[$index]")
+ )
+ }
+ }
+ }
+ }
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ ) {
+ Button(
+ onClick = performReset,
+ enabled = resetButtonEnabled,
+ modifier = Modifier
+ .align(Alignment.CenterStart)
+ .testTag("resetButton")
+ ) {
+ Text(
+ text = localization.driversLicenseApplicationFormReset,
+ modifier = Modifier
+ .testTag("resetButtonText")
+ )
+ }
+ Button(
+ onClick = onApplyButtonClicked,
+ enabled = applyButtonEnabled,
+ modifier = Modifier
+ .align(Alignment.CenterEnd)
+ .testTag("applyButton")
+ ) {
+ Row {
+ if (processingApplication) {
+ CircularProgressIndicator(
+ modifier = Modifier
+ .size(16.dp)
+ .align(Alignment.CenterVertically)
+ .testTag("applyButtonProgress")
+ )
+ Spacer(
+ modifier = Modifier
+ .width(8.dp)
+ )
+ }
+ Text(
+ text = localization.driversLicenseApplicationFormApply,
+ modifier = Modifier
+ .testTag("applyButtonText")
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+@Preview
+private fun PreviewDriversLicenseApplicationForm() {
+ DriversLicenseApplicationFormContent(
+ fieldsEditable = true,
+ name = mutableStateOf(""),
+ onNameChange = {},
+ nameFeedback = emptyList(),
+ age = mutableStateOf(""),
+ onAgeChange = {},
+ ageFeedback = emptyList(),
+ licenseType = null,
+ onLicenseTypeChange = {},
+ licenseTypeFeedback = emptyList(),
+ performReset = {},
+ resetButtonEnabled = false,
+ applyButtonEnabled = true,
+ onApplyButtonClicked = {},
+ processingApplication = false,
+ FocusFirstFocusRequester(
+ listOf(
+ DriversLicenseApplicationModel::name,
+ DriversLicenseApplicationModel::age,
+ DriversLicenseApplicationModel::licenseType
+ )
+ ),
+ localization = EnglishUsLocalization()
+ )
+}
diff --git a/toolkit/samples/dmvapp/dmvapp-gui/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/gui/composable/ValidationFeedback.kt b/toolkit/samples/dmvapp/dmvapp-gui/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/gui/composable/ValidationFeedback.kt
new file mode 100644
index 000000000..5ba52b285
--- /dev/null
+++ b/toolkit/samples/dmvapp/dmvapp-gui/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/gui/composable/ValidationFeedback.kt
@@ -0,0 +1,40 @@
+package tech.coner.trailer.toolkit.sample.dmvapp.gui.composable
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import tech.coner.trailer.toolkit.sample.dmvapp.presentation.localization.Localization
+import tech.coner.trailer.toolkit.sample.dmvapp.presentation.validation.DriversLicenseApplicationModelFeedback
+
+@Composable
+fun validationFeedbackSupportingText(
+ feedback: List,
+ localization: Localization
+): @Composable (() -> Unit) {
+ return when {
+ feedback.isNotEmpty() -> {
+ {
+ ValidationFeedbackSupportingTextContent(
+ feedback
+ .map { localization.message(it) }
+ )
+ }
+ }
+ else -> {
+ {
+ ValidationFeedbackSupportingTextContent(listOf(""))
+ }
+ }
+ }
+}
+
+@Composable
+private fun ValidationFeedbackSupportingTextContent(
+ feedback: List
+) {
+ Column {
+ feedback.forEach {
+ Text(it)
+ }
+ }
+}
\ No newline at end of file
diff --git a/toolkit/samples/dmvapp/dmvapp-gui/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/gui/util/FocusFirstFocusRequester.kt b/toolkit/samples/dmvapp/dmvapp-gui/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/gui/util/FocusFirstFocusRequester.kt
new file mode 100644
index 000000000..982b8f991
--- /dev/null
+++ b/toolkit/samples/dmvapp/dmvapp-gui/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/gui/util/FocusFirstFocusRequester.kt
@@ -0,0 +1,47 @@
+package tech.coner.trailer.toolkit.sample.dmvapp.gui.util
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.onFocusEvent
+import kotlin.reflect.KProperty1
+
+class FocusFirstFocusRequester(
+ internal val store: LinkedHashMap, Entry>
+) {
+
+ fun requestFocus(property: KProperty1) {
+ if (store.values.any { it.focused }) return
+ store[property]?.focusRequester?.requestFocus()
+ }
+
+ class Entry(
+ val focusRequester: FocusRequester,
+ ) {
+ var focused: Boolean = false
+ }
+
+ companion object {
+
+ fun FocusFirstFocusRequester(properties: List>): FocusFirstFocusRequester {
+ return FocusFirstFocusRequester(
+ store = linkedMapOf(
+ *properties.map { it to Entry(FocusRequester()) }.toTypedArray()
+ )
+ )
+ }
+ }
+}
+
+@Composable
+fun Modifier.focusOnError(
+ focusFirstFocusRequester: FocusFirstFocusRequester,
+ property: KProperty1
+): Modifier {
+ val focusRequester: FocusRequester = focusFirstFocusRequester.store[property]?.focusRequester ?: return this
+ return focusRequester(focusRequester)
+ .onFocusEvent {
+ focusFirstFocusRequester.store[property]?.focused = it.isFocused
+ }
+}
\ No newline at end of file
diff --git a/toolkit/samples/dmvapp/dmvapp-gui/src/test/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/gui/composable/DriversLicenseApplicationFormTest.kt b/toolkit/samples/dmvapp/dmvapp-gui/src/test/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/gui/composable/DriversLicenseApplicationFormTest.kt
new file mode 100644
index 000000000..00c8e199a
--- /dev/null
+++ b/toolkit/samples/dmvapp/dmvapp-gui/src/test/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/gui/composable/DriversLicenseApplicationFormTest.kt
@@ -0,0 +1,64 @@
+package tech.coner.trailer.toolkit.sample.dmvapp.gui.composable
+
+import androidx.compose.ui.test.*
+import org.junit.Test
+import org.kodein.di.compose.withDI
+import tech.coner.trailer.toolkit.sample.dmvapp.gui.di
+import tech.coner.trailer.toolkit.sample.dmvapp.gui.page.DriversLicenseApplicationFormPage
+
+class DriversLicenseApplicationFormTest {
+
+ @Test
+ fun showsDriversLicenseFormInInitialState() = runComposeUiTest {
+ setContent {
+ withDI(di) {
+ DriversLicenseApplicationFormScreen()
+ }
+ }
+
+ val page = DriversLicenseApplicationFormPage()
+
+ page.nameField
+ .assertExists()
+ .assertTextEquals("Name")
+ page.name
+ .assertExists()
+ .assertTextEquals("")
+ .assertIsEnabled()
+
+ page.ageField
+ .assertExists()
+ .assertTextEquals("Age")
+ page.age
+ .assertExists()
+ .assertTextEquals("")
+
+ page.licenseTypeField
+ .assertExists()
+ .assertTextEquals("License Type")
+ page.licenseType
+ .assertExists()
+ .assertTextEquals("")
+ .assertIsEnabled()
+ page.licenseTypeDropdownMenu
+ .assertDoesNotExist()
+
+ page.resetButton
+ .assertExists()
+ .assertIsNotEnabled()
+ page.resetButtonText
+ .assertExists()
+ .assertTextEquals("Reset")
+
+ page.applyButton
+ .assertExists()
+ .assertIsEnabled()
+ page.applyButtonText
+ .assertExists()
+ .assertTextEquals("Apply")
+
+ page.applyButtonProgress
+ .assertDoesNotExist()
+ }
+
+}
\ No newline at end of file
diff --git a/toolkit/samples/dmvapp/dmvapp-gui/src/test/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/gui/page/DriversLicenseApplicationFormPage.kt b/toolkit/samples/dmvapp/dmvapp-gui/src/test/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/gui/page/DriversLicenseApplicationFormPage.kt
new file mode 100644
index 000000000..395c72c6a
--- /dev/null
+++ b/toolkit/samples/dmvapp/dmvapp-gui/src/test/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/gui/page/DriversLicenseApplicationFormPage.kt
@@ -0,0 +1,24 @@
+package tech.coner.trailer.toolkit.sample.dmvapp.gui.page
+
+import androidx.compose.ui.test.ComposeUiTest
+import androidx.compose.ui.test.onNodeWithTag
+
+class DriversLicenseApplicationFormPage(
+ private val composeUiTest: ComposeUiTest
+) {
+ val nameField get() = composeUiTest.onNodeWithTag("nameField", useUnmergedTree = true)
+ val name get() = composeUiTest.onNodeWithTag("name", useUnmergedTree = true)
+ val ageField get() = composeUiTest.onNodeWithTag("ageField", useUnmergedTree = true)
+ val age get() = composeUiTest.onNodeWithTag("age", useUnmergedTree = true)
+ val licenseTypeField = composeUiTest.onNodeWithTag("licenseTypeField", useUnmergedTree = true)
+ val licenseType get() = composeUiTest.onNodeWithTag("licenseType", useUnmergedTree = true)
+ val licenseTypeDropdownMenu get() = composeUiTest.onNodeWithTag("licenseTypeDropdownMenu", useUnmergedTree = true)
+ val resetButton get() = composeUiTest.onNodeWithTag("resetButton")
+ val resetButtonText get() = composeUiTest.onNodeWithTag("resetButtonText", useUnmergedTree = true)
+ val applyButton get() = composeUiTest.onNodeWithTag("applyButton")
+ val applyButtonText get() = composeUiTest.onNodeWithTag("applyButtonText", useUnmergedTree = true)
+ val applyButtonProgress get() = composeUiTest.onNodeWithTag("applyButtonProgress", useUnmergedTree = true)
+
+}
+
+fun ComposeUiTest.DriversLicenseApplicationFormPage() = DriversLicenseApplicationFormPage(this)
\ No newline at end of file
diff --git a/toolkit/samples/samples-common/pom.xml b/toolkit/samples/samples-common/pom.xml
index 4a245b088..d8d7cdf48 100644
--- a/toolkit/samples/samples-common/pom.xml
+++ b/toolkit/samples/samples-common/pom.xml
@@ -5,9 +5,9 @@
4.0.0
tech.coner.trailer
- parent
+ buildsrc-kotlin-parent
0.1.0-SNAPSHOT
- ../../../pom.xml
+ ../../../buildsrc/buildsrc-kotlin-parent/pom.xml
toolkit-sample-common
diff --git a/toolkit/util/pom.xml b/toolkit/util/pom.xml
index 20e973e32..42db3ebdf 100644
--- a/toolkit/util/pom.xml
+++ b/toolkit/util/pom.xml
@@ -5,9 +5,9 @@
4.0.0
tech.coner.trailer
- parent
+ buildsrc-kotlin-parent
0.1.0-SNAPSHOT
- ../../pom.xml
+ ../../buildsrc/buildsrc-kotlin-parent/pom.xml
toolkit-util
diff --git a/toolkit/validation/validation-testsupport/pom.xml b/toolkit/validation/validation-testsupport/pom.xml
index ca7f649ef..e37d06a36 100644
--- a/toolkit/validation/validation-testsupport/pom.xml
+++ b/toolkit/validation/validation-testsupport/pom.xml
@@ -5,9 +5,9 @@
4.0.0
tech.coner.trailer
- parent
+ buildsrc-kotlin-parent
0.1.0-SNAPSHOT
- ../../../pom.xml
+ ../../../buildsrc/buildsrc-kotlin-parent/pom.xml
toolkit-validation-testsupport
diff --git a/toolkit/validation/validation/pom.xml b/toolkit/validation/validation/pom.xml
index 8b36089e7..3b262f8df 100644
--- a/toolkit/validation/validation/pom.xml
+++ b/toolkit/validation/validation/pom.xml
@@ -5,9 +5,9 @@
4.0.0
tech.coner.trailer
- parent
+ buildsrc-kotlin-parent
0.1.0-SNAPSHOT
- ../../../pom.xml
+ ../../../buildsrc/buildsrc-kotlin-parent/pom.xml
toolkit-validation
diff --git a/toolkit/validation/validation/src/main/kotlin/tech/coner/trailer/toolkit/validation/ValidationResult.kt b/toolkit/validation/validation/src/main/kotlin/tech/coner/trailer/toolkit/validation/ValidationResult.kt
index d99e25e0d..fef76fa13 100644
--- a/toolkit/validation/validation/src/main/kotlin/tech/coner/trailer/toolkit/validation/ValidationResult.kt
+++ b/toolkit/validation/validation/src/main/kotlin/tech/coner/trailer/toolkit/validation/ValidationResult.kt
@@ -7,12 +7,12 @@ data class ValidationResult(
) {
val isValid: Boolean by lazy {
feedback.isEmpty()
- || feedback.values.all { it.all { feedback -> feedback.severity.valid } }
+ || feedback.values.all { it.isValid }
}
val isInvalid: Boolean by lazy {
feedback.isNotEmpty()
- && feedback.values.any { it.any { feedback -> !feedback.severity.valid } }
+ && feedback.values.any { it.isInvalid }
}
/**
@@ -27,4 +27,10 @@ data class ValidationResult(
else -> null
}
}
-}
\ No newline at end of file
+}
+
+val List.isValid: Boolean
+ get() = all { feedback -> feedback.severity.valid }
+
+val List.isInvalid: Boolean
+ get() = any { feedback -> !feedback.severity.valid }
\ No newline at end of file
diff --git a/webapp-competition/pom.xml b/webapp-competition/pom.xml
index 03154f1b9..90878a25d 100644
--- a/webapp-competition/pom.xml
+++ b/webapp-competition/pom.xml
@@ -3,9 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- parent
+ buildsrc-kotlin-parent
tech.coner.trailer
0.1.0-SNAPSHOT
+ ../buildsrc/buildsrc-kotlin-parent/pom.xml
4.0.0