Add NumberInputField
This commit is contained in:
parent
608cf6d3da
commit
440fa4957c
8 changed files with 282 additions and 142 deletions
|
@ -23,7 +23,9 @@ fun TextFieldOutlinedNumber(
|
||||||
MaterialOutlinedTextField(
|
MaterialOutlinedTextField(
|
||||||
value = value?.toString() ?: "",
|
value = value?.toString() ?: "",
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
onValueChange(it.toLongOrNull())
|
onValueChange(
|
||||||
|
it.takeIf { it.isNotBlank() }?.toLongOrNull(),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
enabled = isEnabled,
|
enabled = isEnabled,
|
||||||
|
|
|
@ -3,8 +3,6 @@ package app.k9mail.core.ui.compose.designsystem.molecule.input
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import app.k9mail.core.ui.compose.designsystem.atom.textfield.TextFieldOutlinedNumber
|
import app.k9mail.core.ui.compose.designsystem.atom.textfield.TextFieldOutlinedNumber
|
||||||
|
@ -21,12 +19,10 @@ fun NumberInput(
|
||||||
errorMessage: String? = null,
|
errorMessage: String? = null,
|
||||||
contentPadding: PaddingValues = inputContentPadding(),
|
contentPadding: PaddingValues = inputContentPadding(),
|
||||||
) {
|
) {
|
||||||
val inputError = remember { mutableStateOf<String?>(null) }
|
|
||||||
|
|
||||||
InputLayout(
|
InputLayout(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
errorMessage = errorMessage ?: inputError.value,
|
errorMessage = errorMessage,
|
||||||
) {
|
) {
|
||||||
TextFieldOutlinedNumber(
|
TextFieldOutlinedNumber(
|
||||||
value = value,
|
value = value,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package app.k9mail.feature.account.setup.domain.input
|
package app.k9mail.feature.account.setup.domain.input
|
||||||
|
|
||||||
import app.k9mail.core.common.domain.usecase.validation.ValidationError
|
import app.k9mail.core.common.domain.usecase.validation.ValidationError
|
||||||
|
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* InputField is an interface defining the state of an input field.
|
* InputField is an interface defining the state of an input field.
|
||||||
|
@ -42,4 +43,6 @@ interface InputField<T> {
|
||||||
fun hasError(): Boolean {
|
fun hasError(): Boolean {
|
||||||
return error != null
|
return error != null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateFromValidationResult(result: ValidationResult): InputField<T>
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
package app.k9mail.feature.account.setup.domain.input
|
||||||
|
|
||||||
|
import app.k9mail.core.common.domain.usecase.validation.ValidationError
|
||||||
|
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
|
||||||
|
|
||||||
|
data class NumberInputField(
|
||||||
|
override val value: Long? = null,
|
||||||
|
override val error: ValidationError? = null,
|
||||||
|
override val isValid: Boolean = false,
|
||||||
|
) : InputField<Long?> {
|
||||||
|
|
||||||
|
override fun updateValue(value: Long?): NumberInputField {
|
||||||
|
return NumberInputField(
|
||||||
|
value = value,
|
||||||
|
error = null,
|
||||||
|
isValid = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateError(error: ValidationError?): NumberInputField {
|
||||||
|
return NumberInputField(
|
||||||
|
value = value,
|
||||||
|
error = error,
|
||||||
|
isValid = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateValidity(isValid: Boolean): NumberInputField {
|
||||||
|
if (isValid == this.isValid) return this
|
||||||
|
|
||||||
|
return NumberInputField(
|
||||||
|
value = value,
|
||||||
|
error = null,
|
||||||
|
isValid = isValid,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateFromValidationResult(result: ValidationResult): NumberInputField {
|
||||||
|
return when (result) {
|
||||||
|
is ValidationResult.Success -> copy(
|
||||||
|
error = null,
|
||||||
|
isValid = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
is ValidationResult.Failure -> copy(
|
||||||
|
error = result.error,
|
||||||
|
isValid = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -34,18 +34,18 @@ data class StringInputField(
|
||||||
isValid = isValid,
|
isValid = isValid,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun StringInputField.fromValidationResult(result: ValidationResult): StringInputField {
|
override fun updateFromValidationResult(result: ValidationResult): StringInputField {
|
||||||
return when (result) {
|
return when (result) {
|
||||||
is ValidationResult.Success -> copy(
|
is ValidationResult.Success -> copy(
|
||||||
error = null,
|
error = null,
|
||||||
isValid = true,
|
isValid = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
is ValidationResult.Failure -> copy(
|
is ValidationResult.Failure -> copy(
|
||||||
error = result.error,
|
error = result.error,
|
||||||
isValid = false,
|
isValid = false,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ package app.k9mail.feature.account.setup.ui.options
|
||||||
|
|
||||||
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
|
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
|
||||||
import app.k9mail.core.ui.compose.common.mvi.BaseViewModel
|
import app.k9mail.core.ui.compose.common.mvi.BaseViewModel
|
||||||
import app.k9mail.feature.account.setup.domain.input.fromValidationResult
|
|
||||||
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract.Effect
|
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract.Effect
|
||||||
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract.Event
|
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract.Event
|
||||||
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract.Event.OnAccountNameChanged
|
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract.Event.OnAccountNameChanged
|
||||||
|
@ -83,9 +82,9 @@ internal class AccountOptionsViewModel(
|
||||||
|
|
||||||
updateState {
|
updateState {
|
||||||
it.copy(
|
it.copy(
|
||||||
accountName = state.value.accountName.fromValidationResult(accountNameResult),
|
accountName = state.value.accountName.updateFromValidationResult(accountNameResult),
|
||||||
displayName = state.value.displayName.fromValidationResult(displayNameResult),
|
displayName = state.value.displayName.updateFromValidationResult(displayNameResult),
|
||||||
emailSignature = state.value.emailSignature.fromValidationResult(emailSignatureResult),
|
emailSignature = state.value.emailSignature.updateFromValidationResult(emailSignatureResult),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,210 @@
|
||||||
|
package app.k9mail.feature.account.setup.domain.input
|
||||||
|
|
||||||
|
import app.k9mail.core.common.domain.usecase.validation.ValidationError
|
||||||
|
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
|
||||||
|
import assertk.Assert
|
||||||
|
import assertk.all
|
||||||
|
import assertk.assertThat
|
||||||
|
import assertk.assertions.isEqualTo
|
||||||
|
import assertk.assertions.isFalse
|
||||||
|
import assertk.assertions.isNull
|
||||||
|
import assertk.assertions.isTrue
|
||||||
|
import assertk.assertions.prop
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.junit.runners.Parameterized
|
||||||
|
|
||||||
|
data class InputFieldTestData<T>(
|
||||||
|
val name: String,
|
||||||
|
val initialState: InputField<T>,
|
||||||
|
val initialValue: T,
|
||||||
|
val initialValueEmpty: T,
|
||||||
|
val initialError: ValidationError?,
|
||||||
|
val initialIsValid: Boolean,
|
||||||
|
val createInitialInput: (value: T, error: ValidationError?, isValid: Boolean) -> InputField<T>,
|
||||||
|
val updatedValue: T,
|
||||||
|
)
|
||||||
|
|
||||||
|
@RunWith(Parameterized::class)
|
||||||
|
class InputFieldTest(
|
||||||
|
val data: InputFieldTestData<Any>,
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should set default values`() {
|
||||||
|
assertThat(data.initialState).all {
|
||||||
|
hasValue(data.initialValueEmpty)
|
||||||
|
hasNoError()
|
||||||
|
isNotValid()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should reset error and isValid when value changed`() {
|
||||||
|
val initialInput = data.createInitialInput(
|
||||||
|
data.initialValue,
|
||||||
|
TestValidationError,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = initialInput.updateValue(data.updatedValue)
|
||||||
|
|
||||||
|
assertThat(result).all {
|
||||||
|
hasValue(data.updatedValue)
|
||||||
|
hasNoError()
|
||||||
|
isNotValid()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should reset isValid when error set`() {
|
||||||
|
val initialInput = data.createInitialInput(
|
||||||
|
data.initialValue,
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = initialInput.updateError(TestValidationError)
|
||||||
|
|
||||||
|
assertThat(result).all {
|
||||||
|
hasValue(data.initialValue)
|
||||||
|
hasError(TestValidationError)
|
||||||
|
isNotValid()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should reset error when valid`() {
|
||||||
|
val initialInput = data.createInitialInput(
|
||||||
|
data.initialValue,
|
||||||
|
TestValidationError,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = initialInput.updateValidity(isValid = true)
|
||||||
|
|
||||||
|
assertThat(result).all {
|
||||||
|
hasValue(data.initialValue)
|
||||||
|
hasNoError()
|
||||||
|
isValid()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should not reset error when invalid`() {
|
||||||
|
val initialInput = data.createInitialInput(
|
||||||
|
data.initialValue,
|
||||||
|
TestValidationError,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = initialInput.updateValidity(isValid = false)
|
||||||
|
|
||||||
|
assertThat(result).all {
|
||||||
|
hasValue(data.initialValue)
|
||||||
|
hasError(TestValidationError)
|
||||||
|
isNotValid()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should change error when error changed`() {
|
||||||
|
val initialInput = data.createInitialInput(
|
||||||
|
data.initialValue,
|
||||||
|
TestValidationError,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = initialInput.updateError(TestValidationError2)
|
||||||
|
|
||||||
|
assertThat(result).all {
|
||||||
|
hasValue(data.initialValue)
|
||||||
|
hasError(TestValidationError2)
|
||||||
|
isNotValid()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should map from success ValidationResult`() {
|
||||||
|
val initialInput = data.createInitialInput(
|
||||||
|
data.initialValue,
|
||||||
|
TestValidationError,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = initialInput.updateFromValidationResult(ValidationResult.Success)
|
||||||
|
|
||||||
|
assertThat(result).all {
|
||||||
|
hasValue(data.initialValue)
|
||||||
|
hasNoError()
|
||||||
|
isValid()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should map from failure ValidationResult`() {
|
||||||
|
val initialInput = data.createInitialInput(
|
||||||
|
data.initialValue,
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = initialInput.updateFromValidationResult(ValidationResult.Failure(TestValidationError))
|
||||||
|
|
||||||
|
assertThat(result).all {
|
||||||
|
hasValue(data.initialValue)
|
||||||
|
hasError(TestValidationError)
|
||||||
|
isNotValid()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Assert<InputField<Any>>.hasValue(value: Any) {
|
||||||
|
prop("value") { InputField<*>::value.call(it) }.isEqualTo(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Assert<InputField<Any>>.hasError(error: ValidationError) {
|
||||||
|
prop("error") { InputField<*>::error.call(it) }.isEqualTo(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Assert<InputField<Any>>.hasNoError() {
|
||||||
|
prop("error") { InputField<*>::error.call(it) }.isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Assert<InputField<Any>>.isValid() {
|
||||||
|
prop("isValid") { InputField<*>::isValid.call(it) }.isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Assert<InputField<Any>>.isNotValid() {
|
||||||
|
prop("isValid") { InputField<*>::isValid.call(it) }.isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
@Parameterized.Parameters(name = "{0}")
|
||||||
|
fun data(): List<InputFieldTestData<*>> = listOf(
|
||||||
|
InputFieldTestData(
|
||||||
|
name = "StringInputField",
|
||||||
|
createInitialInput = { value, error, isValid -> StringInputField(value, error, isValid) },
|
||||||
|
initialState = StringInputField(),
|
||||||
|
initialValue = "input",
|
||||||
|
initialValueEmpty = "",
|
||||||
|
initialError = null,
|
||||||
|
initialIsValid = false,
|
||||||
|
updatedValue = "new value",
|
||||||
|
),
|
||||||
|
InputFieldTestData(
|
||||||
|
name = "NumberInputField",
|
||||||
|
createInitialInput = { value, error, isValid -> NumberInputField(value, error, isValid) },
|
||||||
|
initialState = NumberInputField(),
|
||||||
|
initialValue = 123L,
|
||||||
|
initialValueEmpty = null,
|
||||||
|
initialError = null,
|
||||||
|
initialIsValid = false,
|
||||||
|
updatedValue = 456L,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private object TestValidationError : ValidationError
|
||||||
|
private object TestValidationError2 : ValidationError
|
||||||
|
}
|
|
@ -1,121 +0,0 @@
|
||||||
package app.k9mail.feature.account.setup.domain.input
|
|
||||||
|
|
||||||
import app.k9mail.core.common.domain.usecase.validation.ValidationError
|
|
||||||
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
|
|
||||||
import assertk.all
|
|
||||||
import assertk.assertThat
|
|
||||||
import assertk.assertions.isEqualTo
|
|
||||||
import assertk.assertions.isFalse
|
|
||||||
import assertk.assertions.isNull
|
|
||||||
import assertk.assertions.isTrue
|
|
||||||
import assertk.assertions.prop
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class StringInputFieldTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should set default values`() {
|
|
||||||
val stringInputState = StringInputField()
|
|
||||||
|
|
||||||
assertThat(stringInputState).all {
|
|
||||||
prop(StringInputField::value).isEqualTo("")
|
|
||||||
prop(StringInputField::error).isNull()
|
|
||||||
prop(StringInputField::isValid).isFalse()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should reset errorMessage and isValid when value changed`() {
|
|
||||||
val initialInputState = StringInputField(
|
|
||||||
error = TestValidationError,
|
|
||||||
isValid = false,
|
|
||||||
)
|
|
||||||
|
|
||||||
val result = initialInputState.updateValue("new value")
|
|
||||||
|
|
||||||
assertThat(result).all {
|
|
||||||
prop(StringInputField::value).isEqualTo("new value")
|
|
||||||
prop(StringInputField::error).isNull()
|
|
||||||
prop(StringInputField::isValid).isFalse()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should reset isValid when error set`() {
|
|
||||||
val initialInputState = StringInputField(value = "input", isValid = true)
|
|
||||||
|
|
||||||
val result = initialInputState.updateError(TestValidationError)
|
|
||||||
|
|
||||||
assertThat(result).all {
|
|
||||||
prop(StringInputField::value).isEqualTo("input")
|
|
||||||
prop(StringInputField::error).isEqualTo(TestValidationError)
|
|
||||||
prop(StringInputField::isValid).isFalse()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should reset errorMessage when valid`() {
|
|
||||||
val initialInputState = StringInputField(
|
|
||||||
value = "input",
|
|
||||||
error = TestValidationError,
|
|
||||||
)
|
|
||||||
|
|
||||||
val result = initialInputState.updateValidity(isValid = true)
|
|
||||||
|
|
||||||
assertThat(result).all {
|
|
||||||
prop(StringInputField::value).isEqualTo("input")
|
|
||||||
prop(StringInputField::error).isNull()
|
|
||||||
prop(StringInputField::isValid).isTrue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should not reset errorMessage when invalid`() {
|
|
||||||
val initialInputState = StringInputField(
|
|
||||||
value = "input",
|
|
||||||
error = TestValidationError,
|
|
||||||
)
|
|
||||||
|
|
||||||
val result = initialInputState.updateValidity(isValid = false)
|
|
||||||
|
|
||||||
assertThat(result).all {
|
|
||||||
prop(StringInputField::value).isEqualTo("input")
|
|
||||||
prop(StringInputField::error).isEqualTo(TestValidationError)
|
|
||||||
prop(StringInputField::isValid).isFalse()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should map from success ValidationResult`() {
|
|
||||||
val initialInputState = StringInputField(
|
|
||||||
value = "input",
|
|
||||||
error = TestValidationError,
|
|
||||||
)
|
|
||||||
|
|
||||||
val result = initialInputState.fromValidationResult(ValidationResult.Success)
|
|
||||||
|
|
||||||
assertThat(result).all {
|
|
||||||
prop(StringInputField::value).isEqualTo("input")
|
|
||||||
prop(StringInputField::error).isNull()
|
|
||||||
prop(StringInputField::isValid).isTrue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should map from failure ValidationResult`() {
|
|
||||||
val initialInputState = StringInputField(
|
|
||||||
value = "input",
|
|
||||||
error = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
val result = initialInputState.fromValidationResult(ValidationResult.Failure(TestValidationError))
|
|
||||||
|
|
||||||
assertThat(result).all {
|
|
||||||
prop(StringInputField::value).isEqualTo("input")
|
|
||||||
prop(StringInputField::error).isEqualTo(TestValidationError)
|
|
||||||
prop(StringInputField::isValid).isFalse()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private object TestValidationError : ValidationError
|
|
||||||
}
|
|
Loading…
Reference in a new issue