Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
37 changes: 37 additions & 0 deletions datacapture/sampledata/component_multiple_repeated_group.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"resourceType": "Questionnaire",
"item": [
{
"linkId": "1",
"type": "group",
"text": "Repeated Group",
"repeats": true,
"item": [
{
"linkId": "1-1",
"text": "Sample date question",
"type": "date",
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/entryFormat",
"valueString": "yyyy-mm-dd"
}
]
}
]
},
{
"linkId": "2",
"type": "group",
"text": "Decimal Repeated Group",
"repeats": true,
"item": [
{
"linkId": "2-1",
"text": "Sample decimal question",
"type": "decimal"
}
]
}
]
}
24 changes: 24 additions & 0 deletions datacapture/sampledata/component_non_repeated_group.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"resourceType": "Questionnaire",
"item": [
{
"linkId": "1",
"type": "group",
"text": "Group",
"repeats": false,
"item": [
{
"linkId": "1-1",
"text": "Sample date question",
"type": "date",
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/entryFormat",
"valueString": "yyyy-mm-dd"
}
]
}
]
}
]
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023-2024 Google LLC
* Copyright 2023-2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -28,6 +28,7 @@ import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.RootMatchers
import androidx.test.espresso.matcher.ViewMatchers
Expand Down Expand Up @@ -610,15 +611,22 @@ class QuestionnaireUiEspressoTest {
}
}

@Test
fun test_add_item_button_does_not_exist_for_non_repeated_groups() {
buildFragmentFromQuestionnaire("/component_non_repeated_group.json")
onView(withId(R.id.add_item_to_repeated_group)).check(doesNotExist())
}

@Test
fun test_repeated_group_is_added() {
buildFragmentFromQuestionnaire("/component_repeated_group.json")

onView(withId(R.id.questionnaire_edit_recycler_view))
.perform(
RecyclerViewActions.actionOnItemAtPosition<ViewHolder>(
0,
clickChildViewWithId(R.id.add_item),
1, // 'Add item' is in the second row of the recyclerview with group header as the first
// item
clickChildViewWithId(R.id.add_item_to_repeated_group),
),
)

Expand All @@ -638,6 +646,75 @@ class QuestionnaireUiEspressoTest {
}
}

@Test
fun test_repeated_group_adds_multiple_items() {
buildFragmentFromQuestionnaire("/component_multiple_repeated_group.json")
onView(withId(R.id.questionnaire_edit_recycler_view))
.perform(
RecyclerViewActions.actionOnItemAtPosition<ViewHolder>(
1, // The add button position is 1 (zero-indexed) after the group's header
clickChildViewWithId(R.id.add_item_to_repeated_group),
),
)
.perform(
RecyclerViewActions.actionOnItemAtPosition<ViewHolder>(
3, // The add button new position becomes 3 (zero-indexed) after the group's header,
// repeated item's header and the one item added
clickChildViewWithId(R.id.add_item_to_repeated_group),
),
)

onView(ViewMatchers.withId(R.id.questionnaire_edit_recycler_view)).check {
view,
noViewFoundException,
->
if (noViewFoundException != null) {
throw noViewFoundException
}
assertThat(
(view as RecyclerView).countChildViewOccurrences(
R.id.repeated_group_instance_header_title,
),
)
.isEqualTo(2)
}
}

@Test
fun test_repeated_group_adds_items_for_subsequent() {
buildFragmentFromQuestionnaire("/component_multiple_repeated_group.json")
onView(withId(R.id.questionnaire_edit_recycler_view))
.perform(
RecyclerViewActions.actionOnItemAtPosition<ViewHolder>(
3, // The add button for the second repeated group is at position 3 (zero-indexed), after
// the first group's header (0), the first group's add button (1), and the second
// group's header (2)
clickChildViewWithId(R.id.add_item_to_repeated_group),
),
)
.perform(
RecyclerViewActions.actionOnItemAtPosition<ViewHolder>(
5, // The add button for the second group is now at position 5 after adding one item
clickChildViewWithId(R.id.add_item_to_repeated_group),
),
)

onView(ViewMatchers.withId(R.id.questionnaire_edit_recycler_view)).check {
view,
noViewFoundException,
->
if (noViewFoundException != null) {
throw noViewFoundException
}
assertThat(
(view as RecyclerView).countChildViewOccurrences(
R.id.repeated_group_instance_header_title,
),
)
.isEqualTo(2)
}
}

@Test
fun test_repeated_group_is_deleted() {
buildFragmentFromQuestionnaire(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022-2024 Google LLC
* Copyright 2022-2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -22,7 +22,8 @@ import org.hl7.fhir.r4.model.QuestionnaireResponse
/** Various types of rows that can be used in a Questionnaire RecyclerView. */
internal sealed interface QuestionnaireAdapterItem {
/** A row for a question in a Questionnaire RecyclerView. */
data class Question(val item: QuestionnaireViewItem) : QuestionnaireAdapterItem
data class Question(val item: QuestionnaireViewItem) :
QuestionnaireAdapterItem, ReviewAdapterItem

/** A row for a repeated group response instance's header. */
data class RepeatedGroupHeader(
Expand All @@ -35,6 +36,12 @@ internal sealed interface QuestionnaireAdapterItem {
val title: String,
) : QuestionnaireAdapterItem

data class RepeatedGroupAddButton(
val item: QuestionnaireViewItem,
) : QuestionnaireAdapterItem

data class Navigation(val questionnaireNavigationUIState: QuestionnaireNavigationUIState) :
QuestionnaireAdapterItem
QuestionnaireAdapterItem, ReviewAdapterItem
}

internal sealed interface ReviewAdapterItem
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022-2024 Google LLC
* Copyright 2022-2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -28,6 +28,7 @@ import com.google.android.fhir.datacapture.extensions.itemControl
import com.google.android.fhir.datacapture.extensions.shouldUseDialog
import com.google.android.fhir.datacapture.views.NavigationViewHolder
import com.google.android.fhir.datacapture.views.QuestionnaireViewItem
import com.google.android.fhir.datacapture.views.RepeatedGroupAddItemViewHolder
import com.google.android.fhir.datacapture.views.factories.AttachmentViewHolderFactory
import com.google.android.fhir.datacapture.views.factories.AutoCompleteViewHolderFactory
import com.google.android.fhir.datacapture.views.factories.BooleanChoiceViewHolderFactory
Expand Down Expand Up @@ -80,6 +81,11 @@ internal class QuestionnaireEditAdapter(
),
)
}
ViewType.Type.REPEATED_GROUP_ADD_BUTTON -> {
ViewHolder.RepeatedGroupAddButtonViewHolder(
RepeatedGroupAddItemViewHolder.create(parent),
)
}
}
}

Expand Down Expand Up @@ -138,6 +144,10 @@ internal class QuestionnaireEditAdapter(
holder as ViewHolder.NavigationHolder
holder.viewHolder.bind(item.questionnaireNavigationUIState)
}
is QuestionnaireAdapterItem.RepeatedGroupAddButton -> {
holder as ViewHolder.RepeatedGroupAddButtonViewHolder
holder.viewHolder.bind(item.item)
}
}
}

Expand All @@ -163,6 +173,10 @@ internal class QuestionnaireEditAdapter(
type = ViewType.Type.NAVIGATION
subtype = 0xFFFFFF
}
is QuestionnaireAdapterItem.RepeatedGroupAddButton -> {
type = ViewType.Type.REPEATED_GROUP_ADD_BUTTON
subtype = 0
}
}
return ViewType.from(type = type, subtype = subtype).viewType
}
Expand Down Expand Up @@ -194,6 +208,7 @@ internal class QuestionnaireEditAdapter(
enum class Type {
QUESTION,
REPEATED_GROUP_HEADER,
REPEATED_GROUP_ADD_BUTTON,
NAVIGATION,
}
}
Expand Down Expand Up @@ -296,6 +311,9 @@ internal class QuestionnaireEditAdapter(
ViewHolder(viewHolder.itemView)

class NavigationHolder(val viewHolder: NavigationViewHolder) : ViewHolder(viewHolder.itemView)

class RepeatedGroupAddButtonViewHolder(val viewHolder: RepeatedGroupAddItemViewHolder) :
ViewHolder(viewHolder.itemView)
}

internal companion object {
Expand Down Expand Up @@ -324,6 +342,10 @@ internal object DiffCallbacks {
oldItem.index == newItem.index
}
is QuestionnaireAdapterItem.Navigation -> newItem is QuestionnaireAdapterItem.Navigation
is QuestionnaireAdapterItem.RepeatedGroupAddButton -> {
newItem is QuestionnaireAdapterItem.RepeatedGroupAddButton &&
oldItem.item.hasTheSameItem(newItem.item)
}
}

override fun areContentsTheSame(
Expand Down Expand Up @@ -363,6 +385,12 @@ internal object DiffCallbacks {
newItem is QuestionnaireAdapterItem.Navigation &&
oldItem.questionnaireNavigationUIState == newItem.questionnaireNavigationUIState
}
is QuestionnaireAdapterItem.RepeatedGroupAddButton -> {
newItem is QuestionnaireAdapterItem.RepeatedGroupAddButton &&
oldItem.item.hasTheSameItem(newItem.item) &&
oldItem.item.hasTheSameResponse(newItem.item) &&
oldItem.item.hasTheSameValidationResult(newItem.item)
Comment on lines +390 to +392
Copy link
Collaborator

Choose a reason for hiding this comment

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

have you tested having 2 repeated groups?

}
}
}

Expand Down Expand Up @@ -390,4 +418,25 @@ internal object DiffCallbacks {
oldItem.item.hasTheSameValidationResult(newItem.item)
}
}

val REVIEW_ITEMS =
object : DiffUtil.ItemCallback<ReviewAdapterItem>() {
override fun areItemsTheSame(
oldItem: ReviewAdapterItem,
newItem: ReviewAdapterItem,
): Boolean =
ITEMS.areItemsTheSame(
oldItem as QuestionnaireAdapterItem,
newItem as QuestionnaireAdapterItem,
)

override fun areContentsTheSame(
oldItem: ReviewAdapterItem,
newItem: ReviewAdapterItem,
): Boolean =
ITEMS.areContentsTheSame(
oldItem as QuestionnaireAdapterItem,
newItem as QuestionnaireAdapterItem,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ class QuestionnaireFragment : Fragment() {
// Set items
questionnaireEditRecyclerView.visibility = View.GONE
questionnaireReviewAdapter.submitList(
state.items,
state.items.filterIsInstance<ReviewAdapterItem>(),
)
questionnaireReviewRecyclerView.visibility = View.VISIBLE
reviewModeEditButton.visibility =
Expand Down
Loading
Loading