diff --git a/core/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/core/components/ComponentRegistry.kt b/core/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/core/components/ComponentRegistry.kt index 71fafab2..bb26912b 100644 --- a/core/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/core/components/ComponentRegistry.kt +++ b/core/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/core/components/ComponentRegistry.kt @@ -23,6 +23,7 @@ import com.pega.constellation.sdk.kmp.core.components.ComponentTypes.OneColumn import com.pega.constellation.sdk.kmp.core.components.ComponentTypes.Phone import com.pega.constellation.sdk.kmp.core.components.ComponentTypes.RadioButtons import com.pega.constellation.sdk.kmp.core.components.ComponentTypes.Region +import com.pega.constellation.sdk.kmp.core.components.ComponentTypes.RichText import com.pega.constellation.sdk.kmp.core.components.ComponentTypes.RootContainer import com.pega.constellation.sdk.kmp.core.components.ComponentTypes.SimpleTable import com.pega.constellation.sdk.kmp.core.components.ComponentTypes.SimpleTableManual @@ -61,6 +62,7 @@ import com.pega.constellation.sdk.kmp.core.components.fields.EmailComponent import com.pega.constellation.sdk.kmp.core.components.fields.IntegerComponent import com.pega.constellation.sdk.kmp.core.components.fields.PhoneComponent import com.pega.constellation.sdk.kmp.core.components.fields.RadioButtonsComponent +import com.pega.constellation.sdk.kmp.core.components.fields.RichTextComponent import com.pega.constellation.sdk.kmp.core.components.fields.TextAreaComponent import com.pega.constellation.sdk.kmp.core.components.fields.TextInputComponent import com.pega.constellation.sdk.kmp.core.components.fields.TimeComponent @@ -106,6 +108,7 @@ object ComponentRegistry { Def(SimpleTableManual) { SimpleTableManualComponent(it) }, Def(SimpleTableSelect) { SimpleTableSelectComponent(it) }, Def(TextArea) { TextAreaComponent(it) }, + Def(RichText) { RichTextComponent(it) }, Def(TextInput) { TextInputComponent(it) }, Def(Time) { TimeComponent(it) }, Def(URL) { UrlComponent(it) }, diff --git a/core/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/core/components/ComponentTypes.kt b/core/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/core/components/ComponentTypes.kt index 9fa3f6c8..07764d70 100644 --- a/core/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/core/components/ComponentTypes.kt +++ b/core/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/core/components/ComponentTypes.kt @@ -38,6 +38,7 @@ object ComponentTypes { val Integer = ComponentType("Integer") val Phone = ComponentType("Phone") val RadioButtons = ComponentType("RadioButtons") + val RichText = ComponentType("RichText") val TextArea = ComponentType("TextArea") val TextInput = ComponentType("TextInput") val Time = ComponentType("Time") diff --git a/core/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/core/components/fields/RichText.kt b/core/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/core/components/fields/RichText.kt new file mode 100644 index 00000000..b0893627 --- /dev/null +++ b/core/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/core/components/fields/RichText.kt @@ -0,0 +1,5 @@ +package com.pega.constellation.sdk.kmp.core.components.fields + +import com.pega.constellation.sdk.kmp.core.api.ComponentContext + +class RichTextComponent(context: ComponentContext) : FieldComponent(context) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cd9949c6..57d30c52 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,6 +13,7 @@ androidx-test-junit = "1.3.0" compose-hot-reload = "1.0.0" compose-multiplatform = "1.9.0" # warning: update bumps kotlinx-datetime, which breaks iOS compose-navigation = "2.9.1" +htmlconverter = "1.1.0" kotlin = "2.2.21" kotlinx-coroutines = "1.10.2" kotlinx-datetime = "0.6.2" # warning: update breaks iOS compilation @@ -43,6 +44,7 @@ androidx-ui-test-junit4-android = { group = "androidx.compose.ui", name = "ui-te androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" } androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" } +htmlconverter = { module = "be.digitalia.compose.htmlconverter:htmlconverter", version.ref = "htmlconverter" } kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } diff --git a/scripts/dxcomponents/components/fields/rich-text.component.js b/scripts/dxcomponents/components/fields/rich-text.component.js new file mode 100644 index 00000000..a4ab9973 --- /dev/null +++ b/scripts/dxcomponents/components/fields/rich-text.component.js @@ -0,0 +1,3 @@ +import { FieldBaseComponent } from "./field-base.component.js"; + +export class RichTextComponent extends FieldBaseComponent {} diff --git a/scripts/dxcomponents/mappings/sdk-pega-component-map.js b/scripts/dxcomponents/mappings/sdk-pega-component-map.js index 38b89e5f..310c206a 100644 --- a/scripts/dxcomponents/mappings/sdk-pega-component-map.js +++ b/scripts/dxcomponents/mappings/sdk-pega-component-map.js @@ -42,7 +42,7 @@ import { TimeComponent } from "../components/fields/time.component.js"; import { UrlComponent } from "../components/fields/url.component.js"; // import { UserReferenceComponent } from './_components/field/user-reference/user-reference.component'; // import { ScalarListComponent } from './_components/field/scalar-list/scalar-list.component'; -// import { RichTextComponent } from './_components/field/rich-text/rich-text.component'; +import { RichTextComponent } from "../components/fields/rich-text.component.js"; import { UnsupportedComponent } from "../components/widgets/unsupported.component.js"; // Template components @@ -207,7 +207,7 @@ const pegaSdkComponentMap = { reference: ReferenceComponent, RadioButtons: RadioButtonsComponent, Region: RegionComponent, - // RichText: RichTextComponent, + RichText: RichTextComponent, // RichTextEditor: RichTextEditorComponent, RootContainer: RootContainerComponent, // ScalarList: ScalarListComponent, diff --git a/ui-components-cmp/build.gradle.kts b/ui-components-cmp/build.gradle.kts index 549ed505..90ddb752 100644 --- a/ui-components-cmp/build.gradle.kts +++ b/ui-components-cmp/build.gradle.kts @@ -40,6 +40,7 @@ kotlin { implementation(libs.kotlinx.serialization.json) implementation(libs.table.m3) implementation(libs.compose.dnd) + implementation(libs.htmlconverter) } } diff --git a/ui-components-cmp/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/ui/components/cmp/controls/form/FieldValue.kt b/ui-components-cmp/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/ui/components/cmp/controls/form/FieldValue.kt index 15ab7006..32b9b84d 100644 --- a/ui-components-cmp/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/ui/components/cmp/controls/form/FieldValue.kt +++ b/ui-components-cmp/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/ui/components/cmp/controls/form/FieldValue.kt @@ -6,14 +6,21 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.sp @Composable fun FieldValue(label: String, value: String) { + FieldValue(label, AnnotatedString(value)) +} + +@Composable +fun FieldValue(label: String, value: AnnotatedString) { Column(modifier = Modifier.fillMaxWidth()) { if (label.trim().isNotEmpty()) { Text(label, fontSize = 14.sp, color = Color.Gray) } - Text(value.ifEmpty { "---" }, fontSize = 14.sp) + val annotated = if (value.trim().isNotEmpty()) value else AnnotatedString("---") + Text(annotated, fontSize = 14.sp) } } diff --git a/ui-components-cmp/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/ui/components/cmp/controls/form/RichText.kt b/ui-components-cmp/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/ui/components/cmp/controls/form/RichText.kt new file mode 100644 index 00000000..20c064e1 --- /dev/null +++ b/ui-components-cmp/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/ui/components/cmp/controls/form/RichText.kt @@ -0,0 +1,46 @@ +package com.pega.constellation.sdk.kmp.ui.components.cmp.controls.form + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.TextFieldValue +import be.digitalia.compose.htmlconverter.htmlToAnnotatedString +import com.pega.constellation.sdk.kmp.ui.components.cmp.controls.form.internal.Input + +@Composable +fun RichText( + value: String, + label: String, + modifier: Modifier = Modifier, + helperText: String = "", + validateMessage: String = "", + required: Boolean = false, + disabled: Boolean = false, + readOnly: Boolean = false +) { + if (readOnly) { + // Read-only RichText renders on web as display-only + RichTextFieldValue(label, value) + } else { + Input( + value = TextFieldValue(rememberAnnotated(value)), + label = label, + modifier = modifier, + helperText = helperText, + validateMessage = validateMessage, + required = required, + disabled = disabled, + readOnly = true, // not allowing editing in RichText for now + lines = 3 + ) + } +} + +@Composable +fun RichTextFieldValue(label: String, value: String) { + FieldValue(label, rememberAnnotated(value)) +} + +@Composable +private fun rememberAnnotated(value: String) = + remember(value) { htmlToAnnotatedString(value, compactMode = true) } diff --git a/ui-components-cmp/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/ui/components/cmp/controls/form/internal/Input.kt b/ui-components-cmp/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/ui/components/cmp/controls/form/internal/Input.kt index c5227311..0046e79a 100644 --- a/ui-components-cmp/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/ui/components/cmp/controls/form/internal/Input.kt +++ b/ui-components-cmp/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/ui/components/cmp/controls/form/internal/Input.kt @@ -10,6 +10,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.text.input.TextFieldValue import com.pega.constellation.sdk.kmp.ui.components.cmp.controls.form.Label @Composable @@ -31,11 +32,52 @@ internal fun Input( leadingIcon: @Composable (() -> Unit)? = null, trailingIcon: @Composable (() -> Unit)? = null, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } +) { + Input( + value = TextFieldValue(value), + label = label, + modifier = modifier, + helperText = helperText, + validateMessage = validateMessage, + hideLabel = hideLabel, + placeholder = placeholder, + required = required, + disabled = disabled, + readOnly = readOnly, + onValueChange = onValueChange, + onFocusChange = onFocusChange, + lines = lines, + keyboardOptions = keyboardOptions, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + interactionSource = interactionSource + ) +} + +@Composable +internal fun Input( + value: TextFieldValue, + label: String, + modifier: Modifier = Modifier, + helperText: String = "", + validateMessage: String = "", + hideLabel: Boolean = false, + placeholder: String = "", + required: Boolean = false, + disabled: Boolean = false, + readOnly: Boolean = false, + onValueChange: (String) -> Unit = {}, + onFocusChange: (Boolean) -> Unit = {}, + lines: Int = 1, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } ) { Column(modifier = modifier) { OutlinedTextField( value = value, - onValueChange = onValueChange, + onValueChange = { onValueChange(it.text) }, modifier = Modifier .fillMaxWidth() .onFocusChanged { onFocusChange(it.isFocused) }, diff --git a/ui-renderer-cmp/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/ui/renderer/cmp/ComponentRenderers.kt b/ui-renderer-cmp/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/ui/renderer/cmp/ComponentRenderers.kt index 2b707158..9f8175ac 100644 --- a/ui-renderer-cmp/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/ui/renderer/cmp/ComponentRenderers.kt +++ b/ui-renderer-cmp/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/ui/renderer/cmp/ComponentRenderers.kt @@ -24,6 +24,7 @@ import com.pega.constellation.sdk.kmp.core.components.ComponentTypes.OneColumn import com.pega.constellation.sdk.kmp.core.components.ComponentTypes.Phone import com.pega.constellation.sdk.kmp.core.components.ComponentTypes.RadioButtons import com.pega.constellation.sdk.kmp.core.components.ComponentTypes.Region +import com.pega.constellation.sdk.kmp.core.components.ComponentTypes.RichText import com.pega.constellation.sdk.kmp.core.components.ComponentTypes.RootContainer import com.pega.constellation.sdk.kmp.core.components.ComponentTypes.SimpleTable import com.pega.constellation.sdk.kmp.core.components.ComponentTypes.SimpleTableManual @@ -63,6 +64,7 @@ import com.pega.constellation.sdk.kmp.ui.renderer.cmp.fields.EmailRenderer import com.pega.constellation.sdk.kmp.ui.renderer.cmp.fields.IntegerRenderer import com.pega.constellation.sdk.kmp.ui.renderer.cmp.fields.PhoneRenderer import com.pega.constellation.sdk.kmp.ui.renderer.cmp.fields.RadioButtonsRenderer +import com.pega.constellation.sdk.kmp.ui.renderer.cmp.fields.RichTextRenderer import com.pega.constellation.sdk.kmp.ui.renderer.cmp.fields.TextAreaRenderer import com.pega.constellation.sdk.kmp.ui.renderer.cmp.fields.TextInputRenderer import com.pega.constellation.sdk.kmp.ui.renderer.cmp.fields.TimeRenderer @@ -104,6 +106,7 @@ object ComponentRenderers { RadioButtons to RadioButtonsRenderer(), Region to RegionRenderer(), RootContainer to RootContainerRenderer(), + RichText to RichTextRenderer(), SimpleTable to SimpleTableRenderer(), SimpleTableManual to SimpleTableManualRenderer(), SimpleTableSelect to SimpleTableSelectRenderer(), diff --git a/ui-renderer-cmp/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/ui/renderer/cmp/fields/RichTextRenderer.kt b/ui-renderer-cmp/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/ui/renderer/cmp/fields/RichTextRenderer.kt new file mode 100644 index 00000000..b1edfd0f --- /dev/null +++ b/ui-renderer-cmp/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/ui/renderer/cmp/fields/RichTextRenderer.kt @@ -0,0 +1,30 @@ +package com.pega.constellation.sdk.kmp.ui.renderer.cmp.fields + +import androidx.compose.runtime.Composable +import com.pega.constellation.sdk.kmp.core.components.fields.RichTextComponent +import com.pega.constellation.sdk.kmp.ui.components.cmp.controls.form.RichText +import com.pega.constellation.sdk.kmp.ui.components.cmp.controls.form.RichTextFieldValue +import com.pega.constellation.sdk.kmp.ui.renderer.cmp.ComponentRenderer +import com.pega.constellation.sdk.kmp.ui.renderer.cmp.helpers.WithFieldHelpers + +class RichTextRenderer : ComponentRenderer { + @Composable + override fun RichTextComponent.Render() { + WithFieldHelpers( + displayOnly = { + RichTextFieldValue(label, value) + }, + editable = { + RichText( + value = value, + label = label, + helperText = helperText, + validateMessage = validateMessage, + required = required, + disabled = disabled, + readOnly = readOnly + ) + } + ) + } +}