Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
dd85cb9
ISSUE-137900: SimpleTableSelect WIP POC
marekpelc-pega Nov 17, 2025
3d5447c
TASK-1786159: adding 'alive' flag to block updates from dead components
marekpelc-pega Nov 27, 2025
9d977f5
TASK-1786159: show display value for dropdown in readonly mode
marekpelc-pega Nov 27, 2025
7f44f1d
TASK-1786159: few fixes in simple table
marekpelc-pega Nov 27, 2025
b1a822d
TASK-1828883: adding native component
marekpelc-pega Nov 28, 2025
1f44244
TASK-1786159: add support for add/remove/reorder rows
marekpelc-pega Dec 1, 2025
ff9b40e
TASK-1786159: add support for add/edit row in popup
marekpelc-pega Dec 2, 2025
0a8a6c2
TASK-1786159: change requestBody to requestBodyQueue to support POST/…
marekpelc-pega Dec 2, 2025
0dcce27
TASK-1828883: fixing updateOptions in dropdown
marekpelc-pega Dec 3, 2025
ced4e50
TASK-1828883: simple table manual clean-up
marekpelc-pega Dec 3, 2025
186af9a
TASK-1828883: fix banner component, making 'alive' property optional …
marekpelc-pega Dec 4, 2025
c7b88c5
TASK-1828883: fixing ConstellationSdkBaseTest.kt
marekpelc-pega Dec 5, 2025
de5d6e3
TASK-1828883: change compose-hot-reload version to 1.0.0
marekpelc-pega Dec 5, 2025
8ec85bf
TASK-1832909: Editable table is now extracted as ui-component. (#95)
lukjj-public-pega Dec 6, 2025
231042f
TASK-1828883: fix duplicated labels in view and its template
marekpelc-pega Dec 6, 2025
d80fec4
TASK-1828883: minor refactoring in simple-table-manual.component.js
marekpelc-pega Dec 8, 2025
bc36f2c
TASK-1828883: fixing group test
marekpelc-pega Dec 8, 2025
55b18fb
TASK-1828883: adding banners for modal in SimpleTableManual
marekpelc-pega Dec 9, 2025
bf2b646
TASK-1828883: cleaning simple-table.component.js
marekpelc-pega Dec 9, 2025
18d26c1
TASK-1828883: cleaning template-utils.js
marekpelc-pega Dec 9, 2025
1fb80a9
TASK-1828883: few minor review fixes
marekpelc-pega Dec 12, 2025
cc74416
TASK-1828883: fixing GroupTest
marekpelc-pega Dec 12, 2025
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 @@ -18,12 +18,14 @@ import com.pega.constellation.sdk.kmp.core.components.ComponentTypes.FlowContain
import com.pega.constellation.sdk.kmp.core.components.ComponentTypes.Group
import com.pega.constellation.sdk.kmp.core.components.ComponentTypes.Integer
import com.pega.constellation.sdk.kmp.core.components.ComponentTypes.ListView
import com.pega.constellation.sdk.kmp.core.components.ComponentTypes.ModalViewContainer
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.RootContainer
import com.pega.constellation.sdk.kmp.core.components.ComponentTypes.SimpleTable
import com.pega.constellation.sdk.kmp.core.components.ComponentTypes.SimpleTableManual
import com.pega.constellation.sdk.kmp.core.components.ComponentTypes.SimpleTableSelect
import com.pega.constellation.sdk.kmp.core.components.ComponentTypes.TextArea
import com.pega.constellation.sdk.kmp.core.components.ComponentTypes.TextInput
Expand All @@ -40,10 +42,12 @@ import com.pega.constellation.sdk.kmp.core.components.containers.FieldGroupTempl
import com.pega.constellation.sdk.kmp.core.components.containers.FlowContainerComponent
import com.pega.constellation.sdk.kmp.core.components.containers.GroupComponent
import com.pega.constellation.sdk.kmp.core.components.containers.ListViewComponent
import com.pega.constellation.sdk.kmp.core.components.containers.ModalViewContainerComponent
import com.pega.constellation.sdk.kmp.core.components.containers.OneColumnComponent
import com.pega.constellation.sdk.kmp.core.components.containers.RegionComponent
import com.pega.constellation.sdk.kmp.core.components.containers.RootContainerComponent
import com.pega.constellation.sdk.kmp.core.components.containers.SimpleTableComponent
import com.pega.constellation.sdk.kmp.core.components.containers.SimpleTableManualComponent
import com.pega.constellation.sdk.kmp.core.components.containers.SimpleTableSelectComponent
import com.pega.constellation.sdk.kmp.core.components.containers.ViewComponent
import com.pega.constellation.sdk.kmp.core.components.containers.ViewContainerComponent
Expand Down Expand Up @@ -92,12 +96,14 @@ object ComponentRegistry {
Def(Group) { GroupComponent(it) },
Def(Integer) { IntegerComponent(it) },
Def(ListView) { ListViewComponent(it) },
Def(ModalViewContainer) { ModalViewContainerComponent(it) },
Def(OneColumn) { OneColumnComponent(it) },
Def(Phone) { PhoneComponent(it) },
Def(RadioButtons) { RadioButtonsComponent(it) },
Def(Region) { RegionComponent(it) },
Def(RootContainer) { RootContainerComponent(it) },
Def(SimpleTable) { SimpleTableComponent(it) },
Def(SimpleTableManual) { SimpleTableManualComponent(it) },
Def(SimpleTableSelect) { SimpleTableSelectComponent(it) },
Def(TextArea) { TextAreaComponent(it) },
Def(TextInput) { TextInputComponent(it) },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ object ComponentTypes {
val FlowContainer = ComponentType("FlowContainer")
val Group = ComponentType("Group")
val ListView = ComponentType("ListView")
val ModalViewContainer = ComponentType("ModalViewContainer")
val OneColumn = ComponentType("OneColumn")
val Region = ComponentType("Region")
val RootContainer = ComponentType("RootContainer")
Expand All @@ -22,6 +23,7 @@ object ComponentTypes {

// templates
val SimpleTable = ComponentType("SimpleTable")
val SimpleTableManual = ComponentType("SimpleTableManual")
val SimpleTableSelect = ComponentType("SimpleTableSelect")
val FieldGroupTemplate = ComponentType("FieldGroupTemplate")

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.pega.constellation.sdk.kmp.core.components.containers

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.pega.constellation.sdk.kmp.core.api.ComponentContext
import com.pega.constellation.sdk.kmp.core.api.ComponentEvent
import com.pega.constellation.sdk.kmp.core.api.ComponentId
import com.pega.constellation.sdk.kmp.core.api.HideableComponent
import com.pega.constellation.sdk.kmp.core.components.getBoolean
import com.pega.constellation.sdk.kmp.core.components.getJSONArray
import com.pega.constellation.sdk.kmp.core.components.getString
import com.pega.constellation.sdk.kmp.core.components.widgets.AlertBannerComponent
import com.pega.constellation.sdk.kmp.core.internal.ComponentManagerImpl.Companion.getComponentTyped
import kotlinx.serialization.json.JsonObject

class ModalViewContainerComponent(context: ComponentContext) : ContainerComponent(context), HideableComponent {
override var visible by mutableStateOf(false)
private set
var title by mutableStateOf("")
private set
var cancelButtonLabel by mutableStateOf("")
private set
var submitButtonLabel by mutableStateOf("")
private set
var alertBanners: List<AlertBannerComponent> by mutableStateOf(emptyList())
private set

override fun applyProps(props: JsonObject) {
super.applyProps(props)
visible = props.getBoolean("visible")
title = props.getString("title")
cancelButtonLabel = props.getString("cancelLabel")
submitButtonLabel = props.getString("submitLabel")
val banners = props.getJSONArray("alertBanners")
val bannersIds = banners.mapWithIndex { getString(it).toInt() }
alertBanners =
bannersIds.mapNotNull { context.componentManager.getComponentTyped(ComponentId(it)) }
}

fun onCancelClick() {
context.sendComponentEvent(cancelEvent())
}

fun onSubmitClick() {
context.sendComponentEvent(submitEvent())
}

fun cancelEvent() = ComponentEvent(
type = "ModalViewContainerEvent",
eventData = mapOf("type" to "cancel")
)

fun submitEvent() = ComponentEvent(
type = "ModalViewContainerEvent",
eventData = mapOf("type" to "submit")
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ class RootContainerComponent(context: ComponentContext) : BaseComponent(context)
private set
var dialogConfig: Dialog.Config? by mutableStateOf(null)
private set
var modalViewContainer: ModalViewContainerComponent? by mutableStateOf(null)
private set

fun presentDialog(config: Dialog.Config) {
dialogConfig = config
Expand All @@ -33,8 +35,10 @@ class RootContainerComponent(context: ComponentContext) : BaseComponent(context)

override fun applyProps(props: JsonObject) {
val viewContainerId = ComponentId(props.getString("viewContainer").toInt())
val httpMessagesArray = props.getJSONArray("httpMessages")
viewContainer = context.componentManager.getComponentTyped(viewContainerId)
val modalViewContainerId = ComponentId(props.getString("modalViewContainer").toInt())
modalViewContainer = context.componentManager.getComponentTyped(modalViewContainerId)
val httpMessagesArray = props.getJSONArray("httpMessages")
httpMessages = httpMessagesArray.mapWithIndex {
val httpMessage = getJsonObject(it)
val type = httpMessage.getString("type")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ class SimpleTableComponent(context: ComponentContext) : BaseComponent(context) {

override fun applyProps(props: JsonObject) {
val childId = props.getString("child").toInt()
child = context.componentManager.getComponent(ComponentId(childId))
child = if (childId == -1) null else context.componentManager.getComponent(ComponentId(childId))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package com.pega.constellation.sdk.kmp.core.components.containers

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.pega.constellation.sdk.kmp.core.api.BaseComponent
import com.pega.constellation.sdk.kmp.core.api.Component
import com.pega.constellation.sdk.kmp.core.api.ComponentContext
import com.pega.constellation.sdk.kmp.core.api.ComponentEvent
import com.pega.constellation.sdk.kmp.core.api.ComponentId
import com.pega.constellation.sdk.kmp.core.api.HideableComponent
import com.pega.constellation.sdk.kmp.core.components.getBoolean
import com.pega.constellation.sdk.kmp.core.components.getJSONArray
import com.pega.constellation.sdk.kmp.core.components.getString
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonObject

class SimpleTableManualComponent(context: ComponentContext) : BaseComponent(context), HideableComponent {
override var visible by mutableStateOf(false)
private set
var label: String by mutableStateOf("")
private set
var displayMode by mutableStateOf(DisplayMode.DISPLAY_ONLY)
private set
var allowAddRows by mutableStateOf(true)
private set
var allowReorderRows by mutableStateOf(true)
private set
var addButtonLabel by mutableStateOf("")
private set
var columnNames by mutableStateOf(emptyList<String>())
private set
var rows by mutableStateOf(emptyList<Row>())
private set

override fun applyProps(props: JsonObject) {
visible = props.getBoolean("visible")
label = props.getString("label")
displayMode = DisplayMode.valueOf(props.getString("displayMode"))
allowAddRows = props.getBoolean("allowAddRows")
allowReorderRows = props.getBoolean("allowReorderRows")
addButtonLabel = props.getString("addButtonLabel")
columnNames = getColumnNames(props)
rows = getRows(props)
}

private fun getColumnNames(props: JsonObject): List<String> {
val columnsJsonArray = props.getJSONArray("columnLabels")
return columnsJsonArray.mapWithIndex { getString(it) }
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extra new line

private fun getRows(props: JsonObject): List<Row> =
props.getJSONArray("rows").map { jsonElement ->
val rowJsonObject = jsonElement.jsonObject
val componentIds = rowJsonObject.getJSONArray("cellComponentIds")
val ids = componentIds.mapWithIndex { getString(it).toInt() }
val cellComponents = context.componentManager.getComponents(ids.map { ComponentId(it) })
Row(
cells = cellComponents.map { Cell(it) },
showEditButton = rowJsonObject.getBoolean("showEditButton"),
showDeleteButton = rowJsonObject.getBoolean("showDeleteButton")
)
}

fun addRow() = context.sendComponentEvent(simpleTableManualAddRowEvent())

fun deleteRow(rowId: Int) =
context.sendComponentEvent(simpleTableManualDeleteRowEvent(rowId))

fun reorderRow(fromIndex: Int, toIndex: Int) =
context.sendComponentEvent(simpleTableManualReorderRowEvent(fromIndex, toIndex))

fun editRowInModal(rowId: Int) =
context.sendComponentEvent(simpleTableManualEditRowInModalEvent(rowId))

fun addRowInModal() =
context.sendComponentEvent(simpleTableManualAddRowInModalEvent())

private fun simpleTableManualAddRowEvent() =
ComponentEvent("SimpleTableManualEvent", eventData = mapOf("type" to "addRow"))

private fun simpleTableManualDeleteRowEvent(itemId: Int) =
ComponentEvent(
"SimpleTableManualEvent",
eventData = mapOf("type" to "deleteRow", "rowId" to itemId.toString())
)

private fun simpleTableManualReorderRowEvent(fromIndex: Int, toIndex: Int) =
ComponentEvent(
"SimpleTableManualEvent", eventData = mapOf(
"type" to "reorderRow",
"fromIndex" to fromIndex.toString(), "toIndex" to toIndex.toString()
)
)

private fun simpleTableManualEditRowInModalEvent(rowId: Int) =
ComponentEvent(
"SimpleTableManualEvent", eventData = mapOf(
"type" to "editRowInModal",
"rowId" to rowId.toString()
)
)

private fun simpleTableManualAddRowInModalEvent() =
ComponentEvent(
"SimpleTableManualEvent", eventData = mapOf(
"type" to "addRowInModal"
)
)

data class Row(
val cells: List<Cell>,
val showEditButton: Boolean,
val showDeleteButton: Boolean
)

data class Cell(val component: Component)

enum class DisplayMode {
DISPLAY_ONLY, EDITABLE_IN_MODAL, EDITABLE_IN_ROW
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import com.pega.constellation.sdk.kmp.core.api.ComponentContextImpl
import com.pega.constellation.sdk.kmp.core.api.ComponentType
import com.pega.constellation.sdk.kmp.core.api.HideableComponent
import com.pega.constellation.sdk.kmp.core.components.ComponentTypes
import com.pega.constellation.sdk.kmp.core.components.getBoolean
import com.pega.constellation.sdk.kmp.core.components.getString
import com.pega.constellation.sdk.kmp.core.components.optBoolean
import com.pega.constellation.sdk.kmp.core.components.optString
import com.pega.constellation.sdk.kmp.core.components.widgets.UnsupportedComponent.Cause.MISSING_JAVASCRIPT_IMPLEMENTATION
import com.pega.constellation.sdk.kmp.core.components.widgets.UnsupportedComponent.Cause.UNKNOWN_CAUSE
import kotlinx.serialization.json.JsonObject
Expand All @@ -30,9 +30,9 @@ class UnsupportedComponent(
private set

override fun applyProps(props: JsonObject) {
type = ComponentType(props.getString("type"))
type = ComponentType(props.optString("type", this.type.type))
cause = MISSING_JAVASCRIPT_IMPLEMENTATION
visible = props.getBoolean("visible")
visible = props.optBoolean("visible", visible)
}

enum class Cause {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,14 @@ import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import java.io.ByteArrayInputStream
import java.util.concurrent.atomic.AtomicReference
import java.util.concurrent.ConcurrentLinkedQueue

internal class WebViewNetworkInterceptor(
private val pegaUrl: String,
private val okHttpClient: OkHttpClient,
private val nonDxOkHttpClient: OkHttpClient
) : WebViewInterceptor {
private var requestBody = AtomicReference<String?>(null)
private val requestBodyQueue = ConcurrentLinkedQueue<String>()

override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest) =
runCatching {
Expand All @@ -41,12 +40,12 @@ internal class WebViewNetworkInterceptor(
}

fun setRequestBody(body: String) {
requestBody.set(body)
requestBodyQueue.add(body)
}

private fun OkHttpClient.execute(request: WebResourceRequest): Response {
val body = requestBody.takeIf { request.method in listOf("POST", "PATCH") }
?.getAndSet(null)
val body = requestBodyQueue.takeIf { request.method in listOf("POST", "PATCH") }
?.poll()
?.toRequestBody()
val filteredHeaders = request.requestHeaders.filterNot {
isAuthorizationHeaderUndefined(it.key, it.value) || isHeaderDisallowed(it.key)
Expand Down
4 changes: 3 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ androidx-emoji2 = "1.6.0"
androidx-lifecycle = "2.9.5"
androidx-test = "1.7.0"
androidx-test-junit = "1.3.0"
compose-hot-reload = "1.0.0-rc02"
compose-hot-reload = "1.0.0"
compose-multiplatform = "1.9.0" # warning: update bumps kotlinx-datetime, which breaks iOS
compose-navigation = "2.9.1"
kotlin = "2.2.20"
Expand All @@ -24,6 +24,7 @@ okhttp = "4.12.0"
table = "0.2.3"
uiautomator = "2.3.0"
webkit = "1.14.0"
dnd = "0.3.0"

[libraries]
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
Expand Down Expand Up @@ -55,6 +56,7 @@ oidc-appsupport = { module = "io.github.kalinjul.kotlin.multiplatform:oidc-appsu
oidc-tokenstore = { module = "io.github.kalinjul.kotlin.multiplatform:oidc-tokenstore", version.ref = "oidc" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
table-m3 = { module = "io.github.windedge.table:table-m3", version.ref = "table" }
compose-dnd = { module = "com.mohamedrejeb.dnd:compose-dnd", version.ref = "dnd" }

[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ class GroupTest : ComposeTest(PegaVersion.v24_2_2) {
waitForNode("Lists group heading", substring = true)
waitForNode("List group instructions", substring = true)
waitForNode("cars")
waitForNode("Encryption keys")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've fixed that. It was passing earlier because the table was not actually loaded and we had only 1 label of the view.
Once the table gets loaded we get label twice and that caused this assert to fail (because waitForNode checks for exactly 1 element)
Repeating label is also an issue raised as ISSUE-138617

waitForNode("cars 1")
waitForNodes("Encryption keys", 2) // TODO: label displayed twice due to ISSUE-138617
waitForNodes("AeroCrypt-AuroraCrypt", 2) // No idea why but test sees it twice even if it's once in UI
}

private fun ComposeUiTest.verifyListGroupGone() {
Expand Down
4 changes: 4 additions & 0 deletions scripts/bridge/native-bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ class NativeBridge {
console.error(TAG, `Cannot update component #${id} - missing props.`);
return;
}
if (component.alive != null && component.alive === false) {
console.debug(TAG, `Skipping component update #${id} - component is destroyed.`);
return;
}
let componentProps = component.props;
if (component.pConn) {
componentProps["pConnectPropertyReference"] = this.#getPropertyReference(component.pConn);
Expand Down
6 changes: 6 additions & 0 deletions scripts/dxcomponents/components/base.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export class BaseComponent {
jsComponentPConnect;
compId;
type;
alive = false

constructor(componentsManager, pConn) {
this.pConn = pConn;
Expand All @@ -14,5 +15,10 @@ export class BaseComponent {
this.compId = componentsManager.getNextComponentId();
this.type = pConn.meta.type;
this.utils = new Utils();
this.alive = true;
}

destroy() {
this.alive = false;
}
}
Loading