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