diff --git a/CHANGELOG.md b/CHANGELOG.md index bd18a4e..7602f20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - Adds code editor control - Connects rich text widget to form state +- adds multi-select checkbox controls ## 0.2.0 - 2022-07-05 diff --git a/example/src/jvmMain/resources/kitchenSink/schema.json b/example/src/jvmMain/resources/kitchenSink/schema.json index cd79011..6b82503 100644 --- a/example/src/jvmMain/resources/kitchenSink/schema.json +++ b/example/src/jvmMain/resources/kitchenSink/schema.json @@ -62,6 +62,39 @@ "title": "FooBar" } ] + }, + + "checkboxesEnum": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "foo", + "bar", + "foobar" + ] + } + }, + "checkboxesOneOf": { + "type": "array", + "uniqueItems": true, + "items": { + "oneOf": [ + { + "const": "foo", + "title": "Foo" + }, + { + "const": "bar", + "title": "Bar" + }, + { + "const": "foobar", + "title": "FooBar" + } + ] + } } } }, diff --git a/example/src/jvmMain/resources/kitchenSink/uiSchema.json b/example/src/jvmMain/resources/kitchenSink/uiSchema.json index 1076d5f..f262a40 100644 --- a/example/src/jvmMain/resources/kitchenSink/uiSchema.json +++ b/example/src/jvmMain/resources/kitchenSink/uiSchema.json @@ -61,6 +61,14 @@ "type": "Category", "label": "Multi-Select", "elements": [ + { + "type": "Control", + "scope": "#/properties/strings/properties/checkboxesEnum" + }, + { + "type": "Control", + "scope": "#/properties/strings/properties/checkboxesOneOf" + } ] }, { diff --git a/json-forms-compose/api/android/json-forms-compose.api b/json-forms-compose/api/android/json-forms-compose.api index a74af0c..3812c72 100644 --- a/json-forms-compose/api/android/json-forms-compose.api +++ b/json-forms-compose/api/android/json-forms-compose.api @@ -16,6 +16,8 @@ public final class com/copperleaf/forms/compose/controls/ComposableSingletons$De public static field lambda-15 Lkotlin/jvm/functions/Function3; public static field lambda-16 Lkotlin/jvm/functions/Function3; public static field lambda-17 Lkotlin/jvm/functions/Function3; + public static field lambda-18 Lkotlin/jvm/functions/Function3; + public static field lambda-19 Lkotlin/jvm/functions/Function3; public static field lambda-2 Lkotlin/jvm/functions/Function3; public static field lambda-3 Lkotlin/jvm/functions/Function3; public static field lambda-4 Lkotlin/jvm/functions/Function3; @@ -34,6 +36,8 @@ public final class com/copperleaf/forms/compose/controls/ComposableSingletons$De public final fun getLambda-15$json_forms_compose_release ()Lkotlin/jvm/functions/Function3; public final fun getLambda-16$json_forms_compose_release ()Lkotlin/jvm/functions/Function3; public final fun getLambda-17$json_forms_compose_release ()Lkotlin/jvm/functions/Function3; + public final fun getLambda-18$json_forms_compose_release ()Lkotlin/jvm/functions/Function3; + public final fun getLambda-19$json_forms_compose_release ()Lkotlin/jvm/functions/Function3; public final fun getLambda-2$json_forms_compose_release ()Lkotlin/jvm/functions/Function3; public final fun getLambda-3$json_forms_compose_release ()Lkotlin/jvm/functions/Function3; public final fun getLambda-4$json_forms_compose_release ()Lkotlin/jvm/functions/Function3; @@ -84,6 +88,8 @@ public final class com/copperleaf/forms/compose/controls/ControlScope { public final class com/copperleaf/forms/compose/controls/DefaultControlsKt { public static final fun checkbox (Lcom/copperleaf/forms/core/BooleanControl;)Lcom/copperleaf/forms/compose/form/Registered; + public static final fun checkboxesEnum (Lcom/copperleaf/forms/core/ArrayControl;)Lcom/copperleaf/forms/compose/form/Registered; + public static final fun checkboxesOneOf (Lcom/copperleaf/forms/core/ArrayControl;)Lcom/copperleaf/forms/compose/form/Registered; public static final fun codeEditor (Lcom/copperleaf/forms/core/StringControl;)Lcom/copperleaf/forms/compose/form/Registered; public static final fun control (Lcom/copperleaf/forms/core/ArrayControl;)Lcom/copperleaf/forms/compose/form/Registered; public static final fun control (Lcom/copperleaf/forms/core/IntegerControl;)Lcom/copperleaf/forms/compose/form/Registered; @@ -103,10 +109,14 @@ public final class com/copperleaf/forms/compose/controls/LayoutKt { } public final class com/copperleaf/forms/compose/controls/TestersKt { + public static final fun hasArrayItemProperty (Lcom/copperleaf/forms/core/ui/UiElement$Control;Ljava/lang/String;)Z + public static final fun hasArrayItemType (Lcom/copperleaf/forms/core/ui/UiElement$Control;Lcom/copperleaf/forms/core/StringControl;)Z public static final fun hasSchemaProperty (Lcom/copperleaf/forms/core/ui/UiElement$Control;Ljava/lang/String;)Z public static final fun matchesControlType (Lcom/copperleaf/forms/core/ui/UiElement$Control;Lcom/copperleaf/forms/core/ControlType;)Z public static final fun optionFieldIs (Lcom/copperleaf/forms/core/ui/UiElement$Control;Ljava/lang/String;Ljava/lang/String;)Z public static final fun optionIsEnabled (Lcom/copperleaf/forms/core/ui/UiElement$Control;Ljava/lang/String;)Z + public static final fun schemaPropertyIs (Lcom/copperleaf/forms/core/ui/UiElement$Control;Ljava/lang/String;Ljava/lang/String;)Z + public static final fun schemaPropertyIsEnabled (Lcom/copperleaf/forms/core/ui/UiElement$Control;Ljava/lang/String;)Z } public final class com/copperleaf/forms/compose/elements/ComposableSingletons$DefaultUiElementsKt { diff --git a/json-forms-compose/api/jvm/json-forms-compose.api b/json-forms-compose/api/jvm/json-forms-compose.api index fed0ffe..29d001d 100644 --- a/json-forms-compose/api/jvm/json-forms-compose.api +++ b/json-forms-compose/api/jvm/json-forms-compose.api @@ -9,6 +9,8 @@ public final class com/copperleaf/forms/compose/controls/ComposableSingletons$De public static field lambda-15 Lkotlin/jvm/functions/Function3; public static field lambda-16 Lkotlin/jvm/functions/Function3; public static field lambda-17 Lkotlin/jvm/functions/Function3; + public static field lambda-18 Lkotlin/jvm/functions/Function3; + public static field lambda-19 Lkotlin/jvm/functions/Function3; public static field lambda-2 Lkotlin/jvm/functions/Function3; public static field lambda-3 Lkotlin/jvm/functions/Function3; public static field lambda-4 Lkotlin/jvm/functions/Function3; @@ -27,6 +29,8 @@ public final class com/copperleaf/forms/compose/controls/ComposableSingletons$De public final fun getLambda-15$json_forms_compose ()Lkotlin/jvm/functions/Function3; public final fun getLambda-16$json_forms_compose ()Lkotlin/jvm/functions/Function3; public final fun getLambda-17$json_forms_compose ()Lkotlin/jvm/functions/Function3; + public final fun getLambda-18$json_forms_compose ()Lkotlin/jvm/functions/Function3; + public final fun getLambda-19$json_forms_compose ()Lkotlin/jvm/functions/Function3; public final fun getLambda-2$json_forms_compose ()Lkotlin/jvm/functions/Function3; public final fun getLambda-3$json_forms_compose ()Lkotlin/jvm/functions/Function3; public final fun getLambda-4$json_forms_compose ()Lkotlin/jvm/functions/Function3; @@ -77,6 +81,8 @@ public final class com/copperleaf/forms/compose/controls/ControlScope { public final class com/copperleaf/forms/compose/controls/DefaultControlsKt { public static final fun checkbox (Lcom/copperleaf/forms/core/BooleanControl;)Lcom/copperleaf/forms/compose/form/Registered; + public static final fun checkboxesEnum (Lcom/copperleaf/forms/core/ArrayControl;)Lcom/copperleaf/forms/compose/form/Registered; + public static final fun checkboxesOneOf (Lcom/copperleaf/forms/core/ArrayControl;)Lcom/copperleaf/forms/compose/form/Registered; public static final fun codeEditor (Lcom/copperleaf/forms/core/StringControl;)Lcom/copperleaf/forms/compose/form/Registered; public static final fun control (Lcom/copperleaf/forms/core/ArrayControl;)Lcom/copperleaf/forms/compose/form/Registered; public static final fun control (Lcom/copperleaf/forms/core/IntegerControl;)Lcom/copperleaf/forms/compose/form/Registered; @@ -96,10 +102,14 @@ public final class com/copperleaf/forms/compose/controls/LayoutKt { } public final class com/copperleaf/forms/compose/controls/TestersKt { + public static final fun hasArrayItemProperty (Lcom/copperleaf/forms/core/ui/UiElement$Control;Ljava/lang/String;)Z + public static final fun hasArrayItemType (Lcom/copperleaf/forms/core/ui/UiElement$Control;Lcom/copperleaf/forms/core/StringControl;)Z public static final fun hasSchemaProperty (Lcom/copperleaf/forms/core/ui/UiElement$Control;Ljava/lang/String;)Z public static final fun matchesControlType (Lcom/copperleaf/forms/core/ui/UiElement$Control;Lcom/copperleaf/forms/core/ControlType;)Z public static final fun optionFieldIs (Lcom/copperleaf/forms/core/ui/UiElement$Control;Ljava/lang/String;Ljava/lang/String;)Z public static final fun optionIsEnabled (Lcom/copperleaf/forms/core/ui/UiElement$Control;Ljava/lang/String;)Z + public static final fun schemaPropertyIs (Lcom/copperleaf/forms/core/ui/UiElement$Control;Ljava/lang/String;Ljava/lang/String;)Z + public static final fun schemaPropertyIsEnabled (Lcom/copperleaf/forms/core/ui/UiElement$Control;Ljava/lang/String;)Z } public final class com/copperleaf/forms/compose/elements/ComposableSingletons$DefaultUiElementsKt { diff --git a/json-forms-compose/src/androidMain/kotlin/com/copperleaf/forms/compose/form/registration.android.kt b/json-forms-compose/src/androidMain/kotlin/com/copperleaf/forms/compose/form/registration.android.kt index 854bfb0..81dbda0 100644 --- a/json-forms-compose/src/androidMain/kotlin/com/copperleaf/forms/compose/form/registration.android.kt +++ b/json-forms-compose/src/androidMain/kotlin/com/copperleaf/forms/compose/form/registration.android.kt @@ -2,6 +2,8 @@ package com.copperleaf.forms.compose.form import com.copperleaf.forms.compose.controls.ControlRenderer import com.copperleaf.forms.compose.controls.checkbox +import com.copperleaf.forms.compose.controls.checkboxesEnum +import com.copperleaf.forms.compose.controls.checkboxesOneOf import com.copperleaf.forms.compose.controls.codeEditor import com.copperleaf.forms.compose.controls.control import com.copperleaf.forms.compose.controls.dropdownEnum @@ -31,21 +33,30 @@ import com.copperleaf.forms.core.ui.UiElement public actual fun UiElement.Control.Companion.defaults(): List> = listOf( + // text fields StringControl.control(), StringControl.richText(), StringControl.codeEditor(), + // single-select StringControl.dropdownEnum(), StringControl.dropdownOneOf(), StringControl.radioButtonEnum(), StringControl.radioButtonOneOf(), + // multi-select + ArrayControl.checkboxesEnum(), + ArrayControl.checkboxesOneOf(), + + // number controls IntegerControl.control(), NumberControl.control(), + // boolean controls BooleanControl.checkbox(), BooleanControl.switch(), + // composite controls ObjectControl.control(), ArrayControl.control(), ) diff --git a/json-forms-compose/src/commonMain/kotlin/com/copperleaf/forms/compose/controls/defaultControls.kt b/json-forms-compose/src/commonMain/kotlin/com/copperleaf/forms/compose/controls/defaultControls.kt index 92a727a..566f9fa 100644 --- a/json-forms-compose/src/commonMain/kotlin/com/copperleaf/forms/compose/controls/defaultControls.kt +++ b/json-forms-compose/src/commonMain/kotlin/com/copperleaf/forms/compose/controls/defaultControls.kt @@ -86,7 +86,7 @@ import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive -// StringControls +// Text Field Controls // --------------------------------------------------------------------------------------------------------------------- public fun StringControl.control(): Registered = uiControl { @@ -204,6 +204,9 @@ public fun StringControl.codeEditor(): Registered = uiControl( rank = 20, tester = { hasSchemaProperty("enum") } @@ -220,7 +223,7 @@ public fun StringControl.dropdownEnum(): Registered = remember { + val allOptions: List = remember { control.schemaConfig.arrayAt("enum").map { it.jsonPrimitive.content } } @@ -248,8 +251,8 @@ public fun StringControl.dropdownEnum(): Registered + if (allOptions.isNotEmpty()) { + allOptions.forEach { option -> DropdownMenuItem( onClick = { updateText(TextFieldValue(option)) @@ -283,7 +286,7 @@ public fun StringControl.dropdownOneOf(): Registered> = remember { + val allOptions: List> = remember { control.schemaConfig.arrayAt("oneOf").map { it.jsonObject.string("const") to it.jsonObject.string("title") } @@ -314,8 +317,8 @@ public fun StringControl.dropdownOneOf(): Registered + if (allOptions.isNotEmpty()) { + allOptions.forEach { (const, title) -> DropdownMenuItem( onClick = { updateText(TextFieldValue(const)) @@ -341,12 +344,12 @@ public fun StringControl.radioButtonEnum(): Registered = remember { + val allOptions: List = remember { control.schemaConfig.arrayAt("enum").map { it.jsonPrimitive.content } } Row(Modifier.fillMaxSize(), horizontalArrangement = Arrangement.spacedBy(12.dp)) { - allDropdownOptions.forEach { + allOptions.forEach { Row(Modifier.clickable { updateFormState(it) }) { RadioButton( selected = currentValue == it, @@ -367,14 +370,14 @@ public fun StringControl.radioButtonOneOf(): Registered> = remember { + val allOptions: List> = remember { control.schemaConfig.arrayAt("oneOf").map { it.jsonObject.string("const") to it.jsonObject.string("title") } } Row(Modifier.fillMaxSize(), horizontalArrangement = Arrangement.spacedBy(12.dp)) { - allDropdownOptions.forEach { (const, title) -> + allOptions.forEach { (const, title) -> Row(Modifier.clickable { updateFormState(const) }) { RadioButton( selected = currentValue == const, @@ -387,6 +390,93 @@ public fun StringControl.radioButtonOneOf(): Registered = uiControl( + rank = 20, + tester = { hasSchemaProperty("uniqueItems") && hasArrayItemType(StringControl) && hasArrayItemProperty("enum") } +) { + val selectedValues: List = getTypedValue(emptyList()) { + if (it == JsonNull) { + emptyList() + } else { + it.jsonArray.map { it.jsonPrimitive.content } + } + } + val allOptions: List = remember { + control.schemaConfig.objectAt("items").arrayAt("enum").map { it.jsonPrimitive.content } + } + + Row(Modifier.fillMaxSize(), horizontalArrangement = Arrangement.spacedBy(12.dp)) { + allOptions.forEach { option -> + Row(Modifier.clickable { + if (option in selectedValues) { + sendFormAction( + pointer = dataPointer + "/${selectedValues.indexOf(option)}", + action = JsonPointerAction.RemoveValue, + ) + } else { + sendFormAction( + pointer = dataPointer + "/${selectedValues.size}", + action = JsonPointerAction.SetValue(option), + ) + } + markAsTouched() + }) { + Checkbox( + checked = option in selectedValues, + onCheckedChange = null, + modifier = Modifier.padding(end = 8.dp) + ) + Text(option) + } + } + } +} + +public fun ArrayControl.checkboxesOneOf(): Registered = uiControl( + rank = 21, + tester = { hasSchemaProperty("uniqueItems") && hasArrayItemProperty("oneOf") } +) { + val selectedValues: List = getTypedValue(emptyList()) { + if (it == JsonNull) { + emptyList() + } else { + it.jsonArray.map { it.jsonPrimitive.content } + } + } + val allOptions: List> = remember { + control.schemaConfig.objectAt("items").arrayAt("oneOf").map { it.string("const") to it.string("title") } + } + + Row(Modifier.fillMaxSize(), horizontalArrangement = Arrangement.spacedBy(12.dp)) { + allOptions.forEach { (const, title) -> + Row(Modifier.clickable { + if (const in selectedValues) { + sendFormAction( + pointer = dataPointer + "/${selectedValues.indexOf(const)}", + action = JsonPointerAction.RemoveValue, + ) + } else { + sendFormAction( + pointer = dataPointer + "/${selectedValues.size}", + action = JsonPointerAction.SetValue(const), + ) + } + markAsTouched() + }) { + Checkbox( + checked = const in selectedValues, + onCheckedChange = null, + modifier = Modifier.padding(end = 8.dp) + ) + Text(title) + } + } + } +} + // Number Controls // --------------------------------------------------------------------------------------------------------------------- diff --git a/json-forms-compose/src/commonMain/kotlin/com/copperleaf/forms/compose/controls/testers.kt b/json-forms-compose/src/commonMain/kotlin/com/copperleaf/forms/compose/controls/testers.kt index fbd8687..b800b6c 100644 --- a/json-forms-compose/src/commonMain/kotlin/com/copperleaf/forms/compose/controls/testers.kt +++ b/json-forms-compose/src/commonMain/kotlin/com/copperleaf/forms/compose/controls/testers.kt @@ -1,6 +1,7 @@ package com.copperleaf.forms.compose.controls import com.copperleaf.forms.core.ControlType +import com.copperleaf.forms.core.StringControl import com.copperleaf.forms.core.ui.UiElement import com.copperleaf.json.values.boolean import com.copperleaf.json.values.objectAt @@ -27,3 +28,22 @@ public fun UiElement.Control.optionFieldIs(name: String, value: String): Boolean public fun UiElement.Control.hasSchemaProperty(name: String): Boolean { return schemaConfig.jsonObject.containsKey(name) } + +public fun UiElement.Control.hasArrayItemType(type: StringControl): Boolean { + return schemaConfig + .optional { objectAt("items").string("type") } == type.type +} + +public fun UiElement.Control.hasArrayItemProperty(name: String): Boolean { + return schemaConfig + .optional { objectAt("items").containsKey(name) } == true +} + +public fun UiElement.Control.schemaPropertyIs(name: String, value: String): Boolean { + return schemaConfig.optional { string(name) } == value +} + +public fun UiElement.Control.schemaPropertyIsEnabled(name: String): Boolean { + return schemaConfig + .optional { boolean(name) } == true +} diff --git a/json-forms-compose/src/jvmMain/kotlin/com/copperleaf/forms/compose/form/registration.desktop.kt b/json-forms-compose/src/jvmMain/kotlin/com/copperleaf/forms/compose/form/registration.desktop.kt index 854bfb0..81dbda0 100644 --- a/json-forms-compose/src/jvmMain/kotlin/com/copperleaf/forms/compose/form/registration.desktop.kt +++ b/json-forms-compose/src/jvmMain/kotlin/com/copperleaf/forms/compose/form/registration.desktop.kt @@ -2,6 +2,8 @@ package com.copperleaf.forms.compose.form import com.copperleaf.forms.compose.controls.ControlRenderer import com.copperleaf.forms.compose.controls.checkbox +import com.copperleaf.forms.compose.controls.checkboxesEnum +import com.copperleaf.forms.compose.controls.checkboxesOneOf import com.copperleaf.forms.compose.controls.codeEditor import com.copperleaf.forms.compose.controls.control import com.copperleaf.forms.compose.controls.dropdownEnum @@ -31,21 +33,30 @@ import com.copperleaf.forms.core.ui.UiElement public actual fun UiElement.Control.Companion.defaults(): List> = listOf( + // text fields StringControl.control(), StringControl.richText(), StringControl.codeEditor(), + // single-select StringControl.dropdownEnum(), StringControl.dropdownOneOf(), StringControl.radioButtonEnum(), StringControl.radioButtonOneOf(), + // multi-select + ArrayControl.checkboxesEnum(), + ArrayControl.checkboxesOneOf(), + + // number controls IntegerControl.control(), NumberControl.control(), + // boolean controls BooleanControl.checkbox(), BooleanControl.switch(), + // composite controls ObjectControl.control(), ArrayControl.control(), )