feat: make map and []any config fields editable as JSON text in Android UI

Replace ReadOnlyField with JsonField composable that provides multi-line
JSON editing with real-time validation feedback. Add prettyPrint
serialization for readable display and parseToJsonElement for save,
letting parse errors propagate to the existing error snackbar.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kohei 2026-02-28 02:48:21 +09:00
parent 76d669efb5
commit 7746cafe50
2 changed files with 23 additions and 6 deletions

View file

@ -201,7 +201,7 @@ private fun ConfigField(
"float" -> NumberField(field, onValueChanged, KeyboardType.Decimal)
"[]string" -> StringArrayField(field, onValueChanged)
"directory" -> DirectoryField(field, onValueChanged, snackbarHostState)
"map", "[]any" -> ReadOnlyField(field)
"map", "[]any" -> JsonField(field, onValueChanged)
else -> StringField(field, onValueChanged)
}
}
@ -296,13 +296,26 @@ private fun StringArrayField(field: FieldState, onValueChanged: (String) -> Unit
}
@Composable
private fun ReadOnlyField(field: FieldState) {
private fun JsonField(field: FieldState, onValueChanged: (String) -> Unit) {
val jsonError = remember(field.value) {
if (field.value.isBlank()) null
else try {
kotlinx.serialization.json.Json.parseToJsonElement(field.value); null
} catch (e: Exception) {
e.message
}
}
OutlinedTextField(
value = field.value.ifEmpty { "(complex value)" },
onValueChange = {},
value = field.value,
onValueChange = onValueChanged,
label = { Text(field.label, color = TextSecondary) },
readOnly = true,
enabled = false,
singleLine = false,
minLines = 3,
isError = jsonError != null,
supportingText = if (jsonError != null) {
{ Text(jsonError, color = MaterialTheme.colorScheme.error) }
} else null,
colors = configFieldColors(),
modifier = Modifier.fillMaxWidth(),
)

View file

@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull
@ -29,6 +30,7 @@ class ConfigViewModel(private val apiClient: ConfigApiClient) : ViewModel() {
private var configValues: JsonObject? = null
private var saveJob: Job? = null
private var pendingSectionKey: String? = null
private val prettyJson = Json { prettyPrint = true }
init {
loadData()
@ -193,6 +195,7 @@ class ConfigViewModel(private val apiClient: ConfigApiClient) : ViewModel() {
element.map { it.jsonPrimitive.contentOrNull ?: "" }.joinToString(", ")
} else ""
}
"map", "[]any" -> prettyJson.encodeToString(JsonElement.serializer(), element)
else -> (element as? JsonPrimitive)?.contentOrNull ?: element.toString()
}
}
@ -232,6 +235,7 @@ class ConfigViewModel(private val apiClient: ConfigApiClient) : ViewModel() {
.filter { it.isNotEmpty() }
.map { JsonPrimitive(it) }
)
"map", "[]any" -> prettyJson.parseToJsonElement(field.value)
else -> JsonPrimitive(field.value)
}
}