Skip to content

Commit a02b784

Browse files
TASK-1880461: add support for multiselect in ListView (DataReference … (#118)
Co-authored-by: mloct <tomasz.mlocek@pega.com>
1 parent 562e069 commit a02b784

File tree

18 files changed

+2028
-75
lines changed

18 files changed

+2028
-75
lines changed

core/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/core/components/containers/ListViewComponent.kt

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import com.pega.constellation.sdk.kmp.core.api.ComponentContext
99
import com.pega.constellation.sdk.kmp.core.api.ComponentEvent
1010
import com.pega.constellation.sdk.kmp.core.components.containers.ListViewComponent.SelectionMode.MULTI
1111
import com.pega.constellation.sdk.kmp.core.components.containers.ListViewComponent.SelectionMode.SINGLE
12+
import com.pega.constellation.sdk.kmp.core.components.getBoolean
1213
import com.pega.constellation.sdk.kmp.core.components.getJSONArray
1314
import com.pega.constellation.sdk.kmp.core.components.getString
1415
import kotlinx.serialization.json.JsonArray
@@ -20,14 +21,12 @@ import kotlinx.serialization.json.jsonPrimitive
2021

2122
class ListViewComponent(context: ComponentContext) : BaseComponent(context) {
2223
enum class SelectionMode { SINGLE, MULTI }
23-
data class Item(val data: Map<String, String>)
24+
data class Item(val data: Map<String, String>, val selected: Boolean)
2425

2526
var label by mutableStateOf("")
2627
private set
2728
var selectionMode by mutableStateOf(SINGLE)
2829
private set
29-
var selectedItemIndex: Int? by mutableStateOf(null)
30-
private set
3130
var columnNames by mutableStateOf(emptyList<String>())
3231
private set
3332
var columnLabels by mutableStateOf(emptyList<String>())
@@ -37,37 +36,34 @@ class ListViewComponent(context: ComponentContext) : BaseComponent(context) {
3736

3837
override fun applyProps(props: JsonObject) {
3938
label = props.getString("label")
40-
if (props.selectionMode() != SINGLE) {
41-
Log.w(TAG, "Only SINGLE selection mode is supported. Defaulting to SINGLE.")
42-
}
43-
selectionMode = SINGLE
44-
selectedItemIndex = props.getIntOrNull("selectedItemIndex")
39+
selectionMode = props.selectionMode()
4540
columnNames = props.getJSONArray("columnNames").map { it.jsonPrimitive.content }
4641
columnLabels = props.getJSONArray("columnLabels").map { it.jsonPrimitive.content }
4742
items = props.getJSONArray("items")
4843
.map {
49-
Item(it.toFoldedItemContent(emptyMap(), ""))
44+
Item(
45+
data = it.toFoldedItemContent(emptyMap(), ""),
46+
selected = it.jsonObject.getBoolean("selected")
47+
)
5048
}
5149
}
5250

53-
fun onItemSelected(itemIndex: Int) {
54-
selectedItemIndex = itemIndex
55-
context.sendComponentEvent(itemSelectedEvent(itemIndex))
51+
fun onItemClick(itemIndex: Int, isSelected: Boolean) {
52+
context.sendComponentEvent(clickItemEvent(itemIndex, isSelected))
5653
}
5754

58-
private fun itemSelectedEvent(itemIndex: Int) =
55+
private fun clickItemEvent(itemIndex: Int, isSelected: Boolean) =
5956
ComponentEvent(
60-
type = SELECT_SINGLE_ITEM_EVENT,
61-
mapOf("selectedItemIndex" to itemIndex.toString())
57+
type = CLICK_ITEM_EVENT,
58+
componentData = mapOf(
59+
"clickedItemIndex" to itemIndex.toString(),
60+
"isSelected" to isSelected.toString()
61+
)
6262
)
6363

64-
6564
private fun JsonObject.selectionMode() =
6665
getString("selectionMode").toSelectionMode()
6766

68-
private fun JsonObject.getIntOrNull(key: String): Int? =
69-
get(key)?.jsonPrimitive?.content?.takeIf { it.isNotBlank() }?.toIntOrNull()
70-
7167
private fun JsonElement.toFoldedItemContent(
7268
initialResult: Map<String, String>,
7369
currentPath: String
@@ -99,6 +95,6 @@ class ListViewComponent(context: ComponentContext) : BaseComponent(context) {
9995

10096
companion object {
10197
private const val TAG = "ListViewComponent"
102-
private const val SELECT_SINGLE_ITEM_EVENT = "SelectSingleItem"
98+
private const val CLICK_ITEM_EVENT = "ClickItem"
10399
}
104100
}

samples/android-cmp-app/src/androidInstrumentedTest/kotlin/com/pega/constellation/sdk/kmp/samples/androidcmpapp/test/cases/AutoCompleteTest.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import com.pega.constellation.sdk.kmp.test.mock.PegaVersion
1414
import kotlin.test.Test
1515

1616
@OptIn(ExperimentalTestApi::class)
17-
class AutoCompleteTest : ComposeTest(PegaVersion.v25_1_1) {
17+
class AutoCompleteTest : ComposeTest(PegaVersion.v25_1) {
1818
@Test
1919
fun test_auto_complete() = runComposeUiTest {
2020
setupApp("OFV0MW-Marco-Work-AutoCompleteTest")
@@ -62,16 +62,16 @@ class AutoCompleteTest : ComposeTest(PegaVersion.v25_1_1) {
6262
waitForNode("AutoComplete (", substring = true)
6363
onNodeWithText("Submit").performClick()
6464

65-
waitForNodes("Car Reference", count = 2)
65+
waitForNode("Car Reference")
6666

67-
onAllNodesWithText("Car Reference")[1].performClick()
67+
onNodeWithText("Car Reference").performClick()
6868
waitForNode("Ford")
6969
waitForNode("Fiat")
7070
waitForNode("Toyota")
7171
waitForNode("Skoda")
7272
waitForNode("Audi")
7373

74-
onAllNodesWithText("Car Reference")[1].performTextInput("F")
74+
onNodeWithText("Car Reference").performTextInput("F")
7575
waitForNode("Ford")
7676
waitForNode("Fiat")
7777

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.pega.constellation.sdk.kmp.samples.androidcmpapp.test.cases
2+
3+
import androidx.compose.ui.test.ComposeUiTest
4+
import androidx.compose.ui.test.ExperimentalTestApi
5+
import androidx.compose.ui.test.onNodeWithText
6+
import androidx.compose.ui.test.performClick
7+
import androidx.compose.ui.test.runComposeUiTest
8+
import com.pega.constellation.sdk.kmp.samples.androidcmpapp.test.ComposeTest
9+
import com.pega.constellation.sdk.kmp.samples.androidcmpapp.test.waitForNode
10+
import com.pega.constellation.sdk.kmp.samples.androidcmpapp.test.waitForNodes
11+
import com.pega.constellation.sdk.kmp.test.mock.PegaVersion
12+
import kotlin.test.Test
13+
14+
@OptIn(ExperimentalTestApi::class)
15+
class DataReferenceListOfRecordsTest : ComposeTest(PegaVersion.v25_1) {
16+
val columns = listOf("BRAND", "MODEL")
17+
18+
@Test
19+
fun test_table_simple_table() = runComposeUiTest {
20+
val cars = listOf(
21+
listOf("Ford", "Focus"),
22+
listOf("Toyota", "Corolla"),
23+
listOf("Fiat", "126p"),
24+
listOf("Skoda", "Octavia"),
25+
listOf("Audi", "A5")
26+
)
27+
val cars2 = listOf(
28+
listOf("Ford", "Focus"),
29+
listOf("Fiat", "126p"),
30+
)
31+
32+
setupApp("O40M3A-MarekCo-Work-DataReferenceListOfRecordsTest")
33+
34+
// create case
35+
onNodeWithText("New Service").performClick()
36+
37+
// Editable Table
38+
waitForNode("DataReference ListOfRecords - Table", substring = true)
39+
waitForNodes("DataReferenceListOfRecordsCars", count = 1)
40+
verifyTable(cars)
41+
onNodeWithText("Next").performClick()
42+
43+
// Editable SimpleTable
44+
waitForNode("DataReference ListOfRecords - SimpleTable", substring = true)
45+
waitForNode("DataReferenceListOfRecordsCars")
46+
verifyTable(cars)
47+
onNodeWithText("Next").performClick()
48+
49+
// Readonly Table
50+
waitForNode("DataReference ListOfRecords - Table readonly", substring = true)
51+
waitForNode("DataReferenceListOfRecordsCars")
52+
verifyTable(cars2)
53+
onNodeWithText("Next").performClick()
54+
55+
// Readonly SimpleTable
56+
waitForNode("DataReference ListOfRecords - SimpleTable readonly", substring = true)
57+
waitForNode("DataReferenceListOfRecordsCars")
58+
verifyTable(cars2)
59+
}
60+
61+
private fun ComposeUiTest.verifyTable(data: List<List<String>>) {
62+
columns.forEach { waitForNodes(it, 2) }
63+
data.flatten().forEach { waitForNodes(it, 2) }
64+
}
65+
}

samples/android-cmp-app/src/androidInstrumentedTest/kotlin/com/pega/constellation/sdk/kmp/samples/androidcmpapp/test/cases/DataReferenceTest.kt renamed to samples/android-cmp-app/src/androidInstrumentedTest/kotlin/com/pega/constellation/sdk/kmp/samples/androidcmpapp/test/cases/DataReferenceSingleRecordTest.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import com.pega.constellation.sdk.kmp.test.mock.PegaVersion
1414
import kotlin.test.Test
1515

1616
@OptIn(ExperimentalTestApi::class)
17-
class DataReferenceTest : ComposeTest(PegaVersion.v24_1_0) {
17+
class DataReferenceSingleRecordTest : ComposeTest(PegaVersion.v24_1_0) {
1818
private val columns = listOf("ID", "BRAND", "MODEL", "COLOR")
1919
private val cars = listOf(
2020
listOf("1", "Ford", "Focus", "Silver"),
@@ -33,7 +33,7 @@ class DataReferenceTest : ComposeTest(PegaVersion.v24_1_0) {
3333

3434
// verify form title
3535
waitForNode("Rent a car (D-", substring = true)
36-
waitForNodes("Select your car", count = 2)
36+
waitForNode("Select your car")
3737

3838
// verify columns
3939
columns.forEach { waitForNodes(it, count = 2) }
@@ -57,7 +57,7 @@ class DataReferenceTest : ComposeTest(PegaVersion.v24_1_0) {
5757
onNodeWithText("Next").performClick()
5858

5959
// verify selected car
60-
waitForNodes("Car for rent", count = 2)
60+
waitForNode("Car for rent")
6161
waitForNode("A5")
6262
waitForNode("Car details")
6363
waitForNode("Brand", substring = true)

samples/android-cmp-app/src/androidInstrumentedTest/kotlin/com/pega/constellation/sdk/kmp/samples/androidcmpapp/test/cases/EmbeddedDataSingleRecordTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import java.time.LocalDateTime
1616
import kotlin.test.Test
1717

1818
@OptIn(ExperimentalTestApi::class)
19-
class EmbeddedDataSingleRecordTest : ComposeTest(PegaVersion.v25_1_1) {
19+
class EmbeddedDataSingleRecordTest : ComposeTest(PegaVersion.v25_1) {
2020

2121
@Test
2222
fun test_embedded_data_single_record() = runComposeUiTest {

samples/android-cmp-app/src/androidInstrumentedTest/kotlin/com/pega/constellation/sdk/kmp/samples/androidcmpapp/test/cases/GroupTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ class GroupTest : ComposeTest(PegaVersion.v24_2_2) {
7474
waitForNode("List group instructions", substring = true)
7575
waitForNode("cars")
7676
waitForNode("cars 1")
77-
waitForNodes("Encryption keys", 2) // TODO: label displayed twice due to ISSUE-138617
77+
waitForNode("Encryption keys")
7878
waitForNodes("AeroCrypt-AuroraCrypt", 2) // No idea why but test sees it twice even if it's once in UI
7979
}
8080

scripts/dxcomponents/components/containers/templates/listview/list-view.component.js

Lines changed: 40 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ export class ListViewComponent extends BaseComponent {
1212
displayedColumnsLabels$ = [];
1313
configProps$;
1414
selectionMode;
15-
selectedItemIndex = null;
1615
singleSelectionMode;
1716
multiSelectionMode;
1817
rowID;
@@ -68,12 +67,13 @@ export class ListViewComponent extends BaseComponent {
6867
}
6968

7069
onEvent(event) {
71-
if (event.type === "SelectSingleItem") {
72-
const selectedItemIndex = Number(event.componentData.selectedItemIndex);
73-
this.#fieldOnChange(selectedItemIndex);
74-
} else {
70+
if (event.type === "ClickItem") {
71+
const clickedItemIndex = Number(event.componentData.clickedItemIndex);
72+
const isSelected = event.componentData.isSelected === "true";
73+
this.#fieldOnChange(clickedItemIndex, isSelected);
74+
} else {
7575
console.log(TAG, `onEvent received unsupported event type ${event.type}`);
76-
}
76+
}
7777
}
7878

7979
#shouldReloadData(oldPayload, newPayload) {
@@ -85,23 +85,25 @@ export class ListViewComponent extends BaseComponent {
8585
return JSON.stringify(filterPayload(oldPayload)) !== JSON.stringify(filterPayload(newPayload));
8686
}
8787

88-
#fieldOnChange(selectedItemIndex) {
89-
if (!this.listViewItems || selectedItemIndex < 0 || selectedItemIndex >= this.listViewItems.length) {
88+
#fieldOnChange(clickedItemIndex, isSelected) {
89+
if (!this.listViewItems || clickedItemIndex < 0 || clickedItemIndex >= this.listViewItems.length) {
9090
console.warn(TAG, "Unexpected state when updating selected item index");
9191
return;
9292
}
9393

9494
const selectedObject = {};
9595
if (this.compositeKeys?.length > 1) {
96-
const selectedRow = this.listViewItems[selectedItemIndex];
96+
const selectedRow = this.listViewItems[clickedItemIndex];
9797
this.compositeKeys.forEach((compositeKey) => {
9898
selectedObject[compositeKey] = selectedRow[compositeKey];
9999
});
100100
} else {
101-
selectedObject[this.rowID] = this.listViewItems[selectedItemIndex][this.rowID];
101+
selectedObject[this.rowID] = this.listViewItems[clickedItemIndex][this.rowID];
102+
}
103+
// When $selected is initialised, core-js treats the component as multi-select (not single-select)
104+
if (this.multiSelectionMode) {
105+
selectedObject.$selected = isSelected;
102106
}
103-
104-
this.selectedItemIndex = selectedItemIndex;
105107
this.pConn?.getListActions?.()?.setSelectedRows([selectedObject]);
106108
}
107109

@@ -134,7 +136,7 @@ export class ListViewComponent extends BaseComponent {
134136
this.label = title;
135137

136138
if (!shouldReloadData) {
137-
this.#updateSelectedItemIndex();
139+
this.#updateSelection();
138140
this.#sendPropsUpdate();
139141
return;
140142
}
@@ -161,60 +163,64 @@ export class ListViewComponent extends BaseComponent {
161163
}).then((response) => {
162164
this.listContext = response;
163165
this.#getListData(() => {
164-
this.#updateSelectedItemIndex();
166+
this.#updateSelection();
165167
this.#sendPropsUpdate();
166168
});
167169
});
168170
}
169171

170-
#updateSelectedItemIndex() {
172+
#updateSelection() {
173+
if (this.singleSelectionMode) {
174+
this.#updateSelectedItemSingle();
175+
} else if (this.multiSelectionMode) {
176+
this.#updateSelectedItemsMulti();
177+
}
178+
}
179+
180+
#updateSelectedItemSingle() {
171181
if (this.compositeKeys && this.compositeKeys.length > 1) {
172-
this.#updateSelectedItemIndexForCompositeKeys();
182+
this.#updateSelectedSingleItemForCompositeKeys();
173183
} else {
174-
this.#updateSelectedItemIndexForSingleKey();
184+
this.#updateSelectedSingleItemForSingleKey();
175185
}
176186
}
177187

178-
#updateSelectedItemIndexForCompositeKeys() {
188+
#updateSelectedItemsMulti() {
189+
const readonlyIds = new Set(this.configProps$.readonlyContextList.map(element => element[this.rowID]));
190+
this.listViewItems.forEach((item) => {
191+
item.selected = readonlyIds.has(item[this.rowID]);
192+
});
193+
}
194+
195+
#updateSelectedSingleItemForCompositeKeys() {
179196
if (!this.listViewItems || this.listViewItems.length === 0) {
180-
this.selectedItemIndex = null;
181197
return;
182198
}
183199

184-
const index = this.listViewItems.findIndex((item) => {
185-
return this.compositeKeys.every((key) => {
200+
this.listViewItems.forEach((item) => {
201+
item.selected = this.compositeKeys.every((key) => {
186202
const left = item[key];
187203
const right = this.contextPage?.[key];
188204
return left != null && right != null && left === right;
189205
});
190206
});
191-
if (index == -1) {
192-
console.log(TAG, `No matching item found.`);
193-
}
194-
this.selectedItemIndex = index === -1 ? null : index;
195207
}
196208

197-
#updateSelectedItemIndexForSingleKey() {
209+
#updateSelectedSingleItemForSingleKey() {
198210
if (!this.listViewItems || this.listViewItems.length === 0) {
199-
this.selectedItemIndex = null;
200211
return;
201212
}
202-
const index = this.listViewItems.findIndex((item) => {
213+
this.listViewItems.forEach((item) => {
203214
const left = item[this.rowID];
204215
const right = this.componentValue;
205-
return left != null && right != null && left === right;
216+
item.selected = left != null && right != null && left === right;
206217
});
207-
if (index == -1) {
208-
console.log(TAG, `No matching item found.`);
209-
}
210-
this.selectedItemIndex = index === -1 ? null : index;
211218
}
212219

213220
#sendPropsUpdate() {
214221
this.props = {
215222
label: this.label,
216223
selectionMode: this.selectionMode,
217-
selectedItemIndex: String(this.selectedItemIndex),
218224
columnNames: this.displayedColumns$,
219225
columnLabels: this.displayedColumnsLabels$,
220226
items: this.listViewItems,

scripts/dxcomponents/components/containers/templates/template-utils.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ export function getReferenceList(pConn) {
1616
export const buildFieldsForTable = (configFields, pConnect, options) => {
1717
const { primaryFieldsViewIndex, fields } = options;
1818
// get resolved field labels for primary fields raw config included in configFields
19+
const rawConfigProps = pConnect.getRawConfigProps()
1920
const fieldsLabels = updateFieldLabels(fields, configFields, primaryFieldsViewIndex, pConnect, {
20-
columnsRawConfig: pConnect.getRawConfigProps()?.children?.find(item => item?.name === 'Columns')?.children
21+
columnsRawConfig: getColumnsRawConfig(rawConfigProps) || getColumnsRawConfig(rawConfigProps?.presets[0])
2122
});
2223

2324
return configFields?.map((field, index) => {
@@ -37,6 +38,10 @@ export const buildFieldsForTable = (configFields, pConnect, options) => {
3738
});
3839
};
3940

41+
function getColumnsRawConfig(configProps) {
42+
return configProps?.children?.find(item => item?.name === 'Columns')?.children
43+
}
44+
4045
/**
4146
* This method evaluates whether a row action is allowed based on the provided conditions.
4247
* @param {string|boolean|undefined} rawExpression - The condition for allowing row action.

0 commit comments

Comments
 (0)