Skip to content

use datetime raw value #184

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -199,10 +199,10 @@ internal fun FeatureFormContent(
@Composable
private fun FeatureFormBody(
form: FeatureForm,
fieldStateMap: Map<Int, BaseFieldState>,
fieldStateMap: Map<Int, BaseFieldState<*>>,
groupStateMap: Map<Int, BaseGroupState>,
modifier: Modifier = Modifier,
onFieldDialogRequest: ((BaseFieldState, Int) -> Unit)? = null
onFieldDialogRequest: ((BaseFieldState<*>, Int) -> Unit)? = null
) {
val lazyListState = rememberLazyListState()
Column(
Expand Down Expand Up @@ -290,8 +290,8 @@ internal fun rememberFieldStates(
elements: List<FormElement>,
context: Context,
scope: CoroutineScope
): Map<Int, BaseFieldState> {
val stateMap = mutableMapOf<Int, BaseFieldState>()
): Map<Int, BaseFieldState<*>> {
val stateMap = mutableMapOf<Int, BaseFieldState<*>>()
elements.forEach { element ->
if (element is FieldFormElement) {
val state = when (element.input) {
Expand Down Expand Up @@ -320,8 +320,8 @@ internal fun rememberFieldStates(
val input = element.input as DateTimePickerFormInput
rememberDateTimeFieldState(
field = element,
minEpochMillis = input.min?.toEpochMilli(),
maxEpochMillis = input.max?.toEpochMilli(),
minEpochMillis = input.min,
maxEpochMillis = input.max,
shouldShowTime = input.includeTime,
form = form,
scope = scope
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ import kotlinx.coroutines.flow.flattenMerge
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.stateIn

internal open class FieldProperties(
internal open class FieldProperties<T>(
val label: String,
val placeholder: String,
val description: String,
val value: StateFlow<String>,
val value: StateFlow<T>,
val required: StateFlow<Boolean>,
val editable: StateFlow<Boolean>,
val visible: StateFlow<Boolean>
Expand All @@ -48,9 +48,9 @@ internal open class FieldProperties(
* @param onEditValue a callback to invoke when the user edits result in a change of value. This
* is called on [BaseFieldState.onValueChanged].
*/
internal open class BaseFieldState(
properties: FieldProperties,
initialValue: String = properties.value.value,
internal open class BaseFieldState<T>(
properties: FieldProperties<T>,
initialValue: T = properties.value.value,
scope: CoroutineScope,
protected val onEditValue: (Any?) -> Unit,
) {
Expand All @@ -76,7 +76,7 @@ internal open class BaseFieldState(
* Current value state for the field.
*/
@OptIn(ExperimentalCoroutinesApi::class)
val value: StateFlow<String> = flowOf(_value, properties.value.drop(1))
val value: StateFlow<T> = flowOf(_value, properties.value.drop(1))
.flattenMerge()
.stateIn(scope, SharingStarted.Eagerly, initialValue)

Expand All @@ -98,7 +98,7 @@ internal open class BaseFieldState(
/**
* Callback to update the current value of the FormTextFieldState to the given [input].
*/
open fun onValueChanged(input: String) {
open fun onValueChanged(input: T) {
onEditValue(input)
_value.value = input
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ internal class GroupProperties(

internal class BaseGroupState(
properties: GroupProperties,
val fieldStates: Map<Int, BaseFieldState?>
val fieldStates: Map<Int, BaseFieldState<*>?>
) {
val label = properties.label

Expand All @@ -47,7 +47,7 @@ internal class BaseGroupState(
}

companion object {
fun Saver(fieldStates: Map<Int, BaseFieldState?>): Saver<BaseGroupState, Any> = listSaver(
fun Saver(fieldStates: Map<Int, BaseFieldState<*>?>): Saver<BaseGroupState, Any> = listSaver(
save = {
listOf(it.label, it.description, it.expanded.value)
},
Expand All @@ -69,7 +69,7 @@ internal class BaseGroupState(
@Composable
internal fun rememberBaseGroupState(
groupElement: GroupFormElement,
fieldStates: Map<Int, BaseFieldState?>
fieldStates: Map<Int, BaseFieldState<*>?>
): BaseGroupState = rememberSaveable(
saver = BaseGroupState.Saver(fieldStates)
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import com.arcgismaps.toolkit.featureforms.components.base.FieldProperties
import com.arcgismaps.toolkit.featureforms.components.text.TextFieldProperties
import com.arcgismaps.toolkit.featureforms.utils.editValue
import com.arcgismaps.toolkit.featureforms.utils.fieldType
import com.arcgismaps.toolkit.featureforms.utils.formattedValueFlow
import com.arcgismaps.toolkit.featureforms.utils.valueFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
Expand All @@ -49,7 +49,7 @@ internal open class CodedValueFieldProperties(
val codedValues: List<CodedValue>,
val showNoValueOption: FormInputNoValueOption,
val noValueLabel: String
) : FieldProperties(label, placeholder, description, value, required, editable, visible)
) : FieldProperties<String>(label, placeholder, description, value, required, editable, visible)

/**
* A class to handle the state of a [ComboBoxField]. Essential properties are inherited
Expand All @@ -68,7 +68,7 @@ internal open class CodedValueFieldState(
initialValue: String = properties.value.value,
scope: CoroutineScope,
onEditValue: ((Any?) -> Unit)
) : BaseFieldState(
) : BaseFieldState<String>(
properties = properties,
scope = scope,
initialValue = initialValue,
Expand Down Expand Up @@ -134,7 +134,7 @@ internal open class CodedValueFieldState(
label = formElement.label,
placeholder = formElement.hint,
description = formElement.description,
value = formElement.formattedValueFlow(scope),
value = formElement.valueFlow(scope),
editable = formElement.isEditable,
required = formElement.isRequired,
visible = formElement.isVisible,
Expand Down Expand Up @@ -169,7 +169,7 @@ internal fun rememberCodedValueFieldState(
label = field.label,
placeholder = field.hint,
description = field.description,
value = field.formattedValueFlow(scope),
value = field.valueFlow(scope),
editable = field.isEditable,
required = field.isRequired,
visible = field.isVisible,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import com.arcgismaps.mapping.featureforms.FieldFormElement
import com.arcgismaps.mapping.featureforms.RadioButtonsFormInput
import com.arcgismaps.toolkit.featureforms.utils.editValue
import com.arcgismaps.toolkit.featureforms.utils.fieldType
import com.arcgismaps.toolkit.featureforms.utils.formattedValueFlow
import com.arcgismaps.toolkit.featureforms.utils.valueFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

Expand Down Expand Up @@ -79,7 +79,7 @@ internal class RadioButtonFieldState(
label = formElement.label,
placeholder = formElement.hint,
description = formElement.description,
value = formElement.formattedValueFlow(scope),
value = formElement.valueFlow(scope),
editable = formElement.isEditable,
required = formElement.isRequired,
visible = formElement.isVisible,
Expand Down Expand Up @@ -114,7 +114,7 @@ internal fun rememberRadioButtonFieldState(
label = field.label,
placeholder = field.hint,
description = field.description,
value = field.formattedValueFlow(scope),
value = field.valueFlow(scope),
editable = field.isEditable,
required = field.isRequired,
visible = field.isVisible,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import com.arcgismaps.toolkit.featureforms.components.base.BaseFieldState
import com.arcgismaps.toolkit.featureforms.utils.editValue
import com.arcgismaps.toolkit.featureforms.utils.fieldIsNullable
import com.arcgismaps.toolkit.featureforms.utils.fieldType
import com.arcgismaps.toolkit.featureforms.utils.formattedValueFlow
import com.arcgismaps.toolkit.featureforms.utils.valueFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -121,7 +121,7 @@ internal class SwitchFieldState(
label = formElement.label,
placeholder = formElement.hint,
description = formElement.description,
value = formElement.formattedValueFlow(scope),
value = formElement.valueFlow(scope),
editable = formElement.isEditable,
required = formElement.isRequired,
visible = formElement.isVisible,
Expand Down Expand Up @@ -166,7 +166,7 @@ internal fun rememberSwitchFieldState(
label = field.label,
placeholder = field.hint,
description = field.description,
value = field.formattedValueFlow(scope),
value = field.valueFlow(scope),
editable = field.isEditable,
required = field.isRequired,
visible = field.isVisible,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ internal fun DateTimeField(
) {
val isEditable by state.isEditable.collectAsState()
val isRequired by state.isRequired.collectAsState()
val epochMillis by state.epochMillis.collectAsState()
val instant by state.value.collectAsState()
val interactionSource = remember { MutableInteractionSource() }
// to check if the field was ever focused by the user
var wasFocused by rememberSaveable { mutableStateOf(false) }
Expand All @@ -61,9 +61,12 @@ internal fun DateTimeField(
}

BaseTextField(
text = epochMillis?.formattedDateTime(state.shouldShowTime) ?: "",
text = instant?.formattedDateTime(state.shouldShowTime) ?: "",
onValueChange = {
state.onValueChanged(it)
// the only allowable change is to clear the text
if (it.isEmpty()) {
state.onValueChanged(null)
}
},
modifier = modifier,
readOnly = true,
Expand All @@ -75,7 +78,7 @@ internal fun DateTimeField(
trailingIcon = Icons.Rounded.EditCalendar,
supportingText = {
// if the field was focused and is required, validate the current value
if (wasFocused && isRequired && epochMillis == null) {
if (wasFocused && isRequired && instant == null) {
Text(
text = stringResource(id = R.string.required),
color = MaterialTheme.colorScheme.error
Expand Down Expand Up @@ -111,7 +114,7 @@ private fun DateTimeFieldPreview() {
label = "Launch Date and Time",
placeholder = "",
description = "Enter the date for apollo 11 launch",
value = MutableStateFlow(""),
value = MutableStateFlow(null),
editable = MutableStateFlow(true),
required = MutableStateFlow(false),
visible = MutableStateFlow(true),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

package com.arcgismaps.toolkit.featureforms.components.datetime

import android.util.Log
import androidx.compose.runtime.Composable
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.listSaver
Expand All @@ -31,31 +30,24 @@ import com.arcgismaps.toolkit.featureforms.components.base.FieldProperties
import com.arcgismaps.toolkit.featureforms.components.text.FormTextFieldState
import com.arcgismaps.toolkit.featureforms.components.text.TextFieldProperties
import com.arcgismaps.toolkit.featureforms.utils.editValue
import com.arcgismaps.toolkit.featureforms.utils.formattedValueFlow
import com.arcgismaps.toolkit.featureforms.utils.valueFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException
import java.util.TimeZone
import java.time.Instant

internal class DateTimeFieldProperties(
label: String,
placeholder: String,
description: String,
value: StateFlow<String>,
value: StateFlow<Instant?>,
required: StateFlow<Boolean>,
editable: StateFlow<Boolean>,
visible: StateFlow<Boolean>,
val minEpochMillis: Long?,
val maxEpochMillis: Long?,
val minEpochMillis: Instant?,
val maxEpochMillis: Instant?,
val shouldShowTime: Boolean
) : FieldProperties(label, placeholder, description, value, required, editable, visible)
) : FieldProperties<Instant?>(label, placeholder, description, value, required, editable, visible)

/**
* A class to handle the state of a [DateTimeField]. Essential properties are inherited from the
Expand All @@ -70,34 +62,21 @@ internal class DateTimeFieldProperties(
*/
internal class DateTimeFieldState(
properties: DateTimeFieldProperties,
initialValue: String = properties.value.value,
initialValue: Instant? = properties.value.value,
scope: CoroutineScope,
onEditValue: (Any?) -> Unit
) : BaseFieldState(
) : BaseFieldState<Instant?>(
properties = properties,
initialValue = initialValue,
scope = scope,
onEditValue = onEditValue
) {
val minEpochMillis: Long? = properties.minEpochMillis
val minEpochMillis: Instant? = properties.minEpochMillis

val maxEpochMillis: Long? = properties.maxEpochMillis
val maxEpochMillis: Instant? = properties.maxEpochMillis

val shouldShowTime: Boolean = properties.shouldShowTime

@OptIn(ExperimentalCoroutinesApi::class)
val epochMillis: StateFlow<Long?> = value.mapLatest {
if (it.toLongOrNull() != null) {
it.toLong()
} else {
dateTimeFromString(it)
}
}.stateIn(
scope,
started = SharingStarted.Eagerly,
initialValue = dateTimeFromString(value.value)
)


companion object {
fun Saver(
field: FieldFormElement,
Expand All @@ -114,12 +93,12 @@ internal class DateTimeFieldState(
label = field.label,
placeholder = field.hint,
description = field.description,
value = field.formattedValueFlow(scope),
value = field.valueFlow(scope),
editable = field.isEditable,
required = field.isRequired,
visible = field.isVisible,
minEpochMillis = input.min?.toEpochMilli(),
maxEpochMillis = input.max?.toEpochMilli(),
minEpochMillis = input.min,
maxEpochMillis = input.max,
shouldShowTime = input.includeTime
),
initialValue = list[0],
Expand All @@ -137,8 +116,8 @@ internal class DateTimeFieldState(
@Composable
internal fun rememberDateTimeFieldState(
field: FieldFormElement,
minEpochMillis: Long?,
maxEpochMillis: Long?,
minEpochMillis: Instant?,
maxEpochMillis: Instant?,
shouldShowTime: Boolean,
form: FeatureForm,
scope: CoroutineScope
Expand All @@ -154,7 +133,7 @@ internal fun rememberDateTimeFieldState(
label = field.label,
placeholder = field.hint,
description = field.description,
value = field.formattedValueFlow(scope),
value = field.valueFlow(scope),
editable = field.isEditable,
required = field.isRequired,
visible = field.isVisible,
Expand All @@ -170,27 +149,3 @@ internal fun rememberDateTimeFieldState(
)
}

/**
* Maps the [FieldFormElement.value] from a String to Long?
* Empty strings are made to be null Longs.
*
* @since 200.3.0
*/
internal fun dateTimeFromString(formattedDateTime: String): Long? {
return if (formattedDateTime.isNotEmpty()) {
try {
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")
LocalDateTime.parse(formattedDateTime, formatter)
.atZone(TimeZone.getDefault().toZoneId())
.toInstant()
.toEpochMilli()
} catch (ex: DateTimeParseException) {
Log.e(
"DateTimeFieldState",
"dateTimeFromString: Error parsing $formattedDateTime into a valid date time",
ex
)
null
}
} else null
}
Loading