Skip to content

Commit fa8ce8f

Browse files
authored
Move the add button for repeated groups to bottom (#2868)
* Move the add button for repeated groups to bottom * Resolve PR comments * Add ReviewAdapterItem for review items * Add tests for multiple repeated group items * Simplify item VIEW_TYPE_ values
1 parent b6e84bb commit fa8ce8f

File tree

15 files changed

+426
-144
lines changed

15 files changed

+426
-144
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"resourceType": "Questionnaire",
3+
"item": [
4+
{
5+
"linkId": "1",
6+
"type": "group",
7+
"text": "Repeated Group",
8+
"repeats": true,
9+
"item": [
10+
{
11+
"linkId": "1-1",
12+
"text": "Sample date question",
13+
"type": "date",
14+
"extension": [
15+
{
16+
"url": "http://hl7.org/fhir/StructureDefinition/entryFormat",
17+
"valueString": "yyyy-mm-dd"
18+
}
19+
]
20+
}
21+
]
22+
},
23+
{
24+
"linkId": "2",
25+
"type": "group",
26+
"text": "Decimal Repeated Group",
27+
"repeats": true,
28+
"item": [
29+
{
30+
"linkId": "2-1",
31+
"text": "Sample decimal question",
32+
"type": "decimal"
33+
}
34+
]
35+
}
36+
]
37+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"resourceType": "Questionnaire",
3+
"item": [
4+
{
5+
"linkId": "1",
6+
"type": "group",
7+
"text": "Group",
8+
"repeats": false,
9+
"item": [
10+
{
11+
"linkId": "1-1",
12+
"text": "Sample date question",
13+
"type": "date",
14+
"extension": [
15+
{
16+
"url": "http://hl7.org/fhir/StructureDefinition/entryFormat",
17+
"valueString": "yyyy-mm-dd"
18+
}
19+
]
20+
}
21+
]
22+
}
23+
]
24+
}

datacapture/src/androidTest/java/com/google/android/fhir/datacapture/test/QuestionnaireUiEspressoTest.kt

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023-2024 Google LLC
2+
* Copyright 2023-2025 Google LLC
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -28,6 +28,7 @@ import androidx.test.espresso.ViewAction
2828
import androidx.test.espresso.action.ViewActions
2929
import androidx.test.espresso.action.ViewActions.typeText
3030
import androidx.test.espresso.assertion.ViewAssertions
31+
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
3132
import androidx.test.espresso.contrib.RecyclerViewActions
3233
import androidx.test.espresso.matcher.RootMatchers
3334
import androidx.test.espresso.matcher.ViewMatchers
@@ -610,15 +611,22 @@ class QuestionnaireUiEspressoTest {
610611
}
611612
}
612613

614+
@Test
615+
fun test_add_item_button_does_not_exist_for_non_repeated_groups() {
616+
buildFragmentFromQuestionnaire("/component_non_repeated_group.json")
617+
onView(withId(R.id.add_item_to_repeated_group)).check(doesNotExist())
618+
}
619+
613620
@Test
614621
fun test_repeated_group_is_added() {
615622
buildFragmentFromQuestionnaire("/component_repeated_group.json")
616623

617624
onView(withId(R.id.questionnaire_edit_recycler_view))
618625
.perform(
619626
RecyclerViewActions.actionOnItemAtPosition<ViewHolder>(
620-
0,
621-
clickChildViewWithId(R.id.add_item),
627+
1, // 'Add item' is in the second row of the recyclerview with group header as the first
628+
// item
629+
clickChildViewWithId(R.id.add_item_to_repeated_group),
622630
),
623631
)
624632

@@ -638,6 +646,75 @@ class QuestionnaireUiEspressoTest {
638646
}
639647
}
640648

649+
@Test
650+
fun test_repeated_group_adds_multiple_items() {
651+
buildFragmentFromQuestionnaire("/component_multiple_repeated_group.json")
652+
onView(withId(R.id.questionnaire_edit_recycler_view))
653+
.perform(
654+
RecyclerViewActions.actionOnItemAtPosition<ViewHolder>(
655+
1, // The add button position is 1 (zero-indexed) after the group's header
656+
clickChildViewWithId(R.id.add_item_to_repeated_group),
657+
),
658+
)
659+
.perform(
660+
RecyclerViewActions.actionOnItemAtPosition<ViewHolder>(
661+
3, // The add button new position becomes 3 (zero-indexed) after the group's header,
662+
// repeated item's header and the one item added
663+
clickChildViewWithId(R.id.add_item_to_repeated_group),
664+
),
665+
)
666+
667+
onView(ViewMatchers.withId(R.id.questionnaire_edit_recycler_view)).check {
668+
view,
669+
noViewFoundException,
670+
->
671+
if (noViewFoundException != null) {
672+
throw noViewFoundException
673+
}
674+
assertThat(
675+
(view as RecyclerView).countChildViewOccurrences(
676+
R.id.repeated_group_instance_header_title,
677+
),
678+
)
679+
.isEqualTo(2)
680+
}
681+
}
682+
683+
@Test
684+
fun test_repeated_group_adds_items_for_subsequent() {
685+
buildFragmentFromQuestionnaire("/component_multiple_repeated_group.json")
686+
onView(withId(R.id.questionnaire_edit_recycler_view))
687+
.perform(
688+
RecyclerViewActions.actionOnItemAtPosition<ViewHolder>(
689+
3, // The add button for the second repeated group is at position 3 (zero-indexed), after
690+
// the first group's header (0), the first group's add button (1), and the second
691+
// group's header (2)
692+
clickChildViewWithId(R.id.add_item_to_repeated_group),
693+
),
694+
)
695+
.perform(
696+
RecyclerViewActions.actionOnItemAtPosition<ViewHolder>(
697+
5, // The add button for the second group is now at position 5 after adding one item
698+
clickChildViewWithId(R.id.add_item_to_repeated_group),
699+
),
700+
)
701+
702+
onView(ViewMatchers.withId(R.id.questionnaire_edit_recycler_view)).check {
703+
view,
704+
noViewFoundException,
705+
->
706+
if (noViewFoundException != null) {
707+
throw noViewFoundException
708+
}
709+
assertThat(
710+
(view as RecyclerView).countChildViewOccurrences(
711+
R.id.repeated_group_instance_header_title,
712+
),
713+
)
714+
.isEqualTo(2)
715+
}
716+
}
717+
641718
@Test
642719
fun test_repeated_group_is_deleted() {
643720
buildFragmentFromQuestionnaire(

datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireAdapterItem.kt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022-2024 Google LLC
2+
* Copyright 2022-2025 Google LLC
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -22,7 +22,8 @@ import org.hl7.fhir.r4.model.QuestionnaireResponse
2222
/** Various types of rows that can be used in a Questionnaire RecyclerView. */
2323
internal sealed interface QuestionnaireAdapterItem {
2424
/** A row for a question in a Questionnaire RecyclerView. */
25-
data class Question(val item: QuestionnaireViewItem) : QuestionnaireAdapterItem
25+
data class Question(val item: QuestionnaireViewItem) :
26+
QuestionnaireAdapterItem, ReviewAdapterItem
2627

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

39+
data class RepeatedGroupAddButton(
40+
val item: QuestionnaireViewItem,
41+
) : QuestionnaireAdapterItem
42+
3843
data class Navigation(val questionnaireNavigationUIState: QuestionnaireNavigationUIState) :
39-
QuestionnaireAdapterItem
44+
QuestionnaireAdapterItem, ReviewAdapterItem
4045
}
46+
47+
internal sealed interface ReviewAdapterItem

datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireEditAdapter.kt

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022-2024 Google LLC
2+
* Copyright 2022-2025 Google LLC
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -28,6 +28,7 @@ import com.google.android.fhir.datacapture.extensions.itemControl
2828
import com.google.android.fhir.datacapture.extensions.shouldUseDialog
2929
import com.google.android.fhir.datacapture.views.NavigationViewHolder
3030
import com.google.android.fhir.datacapture.views.QuestionnaireViewItem
31+
import com.google.android.fhir.datacapture.views.RepeatedGroupAddItemViewHolder
3132
import com.google.android.fhir.datacapture.views.factories.AttachmentViewHolderFactory
3233
import com.google.android.fhir.datacapture.views.factories.AutoCompleteViewHolderFactory
3334
import com.google.android.fhir.datacapture.views.factories.BooleanChoiceViewHolderFactory
@@ -80,6 +81,11 @@ internal class QuestionnaireEditAdapter(
8081
),
8182
)
8283
}
84+
ViewType.Type.REPEATED_GROUP_ADD_BUTTON -> {
85+
ViewHolder.RepeatedGroupAddButtonViewHolder(
86+
RepeatedGroupAddItemViewHolder.create(parent),
87+
)
88+
}
8389
}
8490
}
8591

@@ -138,6 +144,10 @@ internal class QuestionnaireEditAdapter(
138144
holder as ViewHolder.NavigationHolder
139145
holder.viewHolder.bind(item.questionnaireNavigationUIState)
140146
}
147+
is QuestionnaireAdapterItem.RepeatedGroupAddButton -> {
148+
holder as ViewHolder.RepeatedGroupAddButtonViewHolder
149+
holder.viewHolder.bind(item.item)
150+
}
141151
}
142152
}
143153

@@ -163,6 +173,10 @@ internal class QuestionnaireEditAdapter(
163173
type = ViewType.Type.NAVIGATION
164174
subtype = 0xFFFFFF
165175
}
176+
is QuestionnaireAdapterItem.RepeatedGroupAddButton -> {
177+
type = ViewType.Type.REPEATED_GROUP_ADD_BUTTON
178+
subtype = 0
179+
}
166180
}
167181
return ViewType.from(type = type, subtype = subtype).viewType
168182
}
@@ -194,6 +208,7 @@ internal class QuestionnaireEditAdapter(
194208
enum class Type {
195209
QUESTION,
196210
REPEATED_GROUP_HEADER,
211+
REPEATED_GROUP_ADD_BUTTON,
197212
NAVIGATION,
198213
}
199214
}
@@ -296,6 +311,9 @@ internal class QuestionnaireEditAdapter(
296311
ViewHolder(viewHolder.itemView)
297312

298313
class NavigationHolder(val viewHolder: NavigationViewHolder) : ViewHolder(viewHolder.itemView)
314+
315+
class RepeatedGroupAddButtonViewHolder(val viewHolder: RepeatedGroupAddItemViewHolder) :
316+
ViewHolder(viewHolder.itemView)
299317
}
300318

301319
internal companion object {
@@ -324,6 +342,10 @@ internal object DiffCallbacks {
324342
oldItem.index == newItem.index
325343
}
326344
is QuestionnaireAdapterItem.Navigation -> newItem is QuestionnaireAdapterItem.Navigation
345+
is QuestionnaireAdapterItem.RepeatedGroupAddButton -> {
346+
newItem is QuestionnaireAdapterItem.RepeatedGroupAddButton &&
347+
oldItem.item.hasTheSameItem(newItem.item)
348+
}
327349
}
328350

329351
override fun areContentsTheSame(
@@ -363,6 +385,12 @@ internal object DiffCallbacks {
363385
newItem is QuestionnaireAdapterItem.Navigation &&
364386
oldItem.questionnaireNavigationUIState == newItem.questionnaireNavigationUIState
365387
}
388+
is QuestionnaireAdapterItem.RepeatedGroupAddButton -> {
389+
newItem is QuestionnaireAdapterItem.RepeatedGroupAddButton &&
390+
oldItem.item.hasTheSameItem(newItem.item) &&
391+
oldItem.item.hasTheSameResponse(newItem.item) &&
392+
oldItem.item.hasTheSameValidationResult(newItem.item)
393+
}
366394
}
367395
}
368396

@@ -390,4 +418,25 @@ internal object DiffCallbacks {
390418
oldItem.item.hasTheSameValidationResult(newItem.item)
391419
}
392420
}
421+
422+
val REVIEW_ITEMS =
423+
object : DiffUtil.ItemCallback<ReviewAdapterItem>() {
424+
override fun areItemsTheSame(
425+
oldItem: ReviewAdapterItem,
426+
newItem: ReviewAdapterItem,
427+
): Boolean =
428+
ITEMS.areItemsTheSame(
429+
oldItem as QuestionnaireAdapterItem,
430+
newItem as QuestionnaireAdapterItem,
431+
)
432+
433+
override fun areContentsTheSame(
434+
oldItem: ReviewAdapterItem,
435+
newItem: ReviewAdapterItem,
436+
): Boolean =
437+
ITEMS.areContentsTheSame(
438+
oldItem as QuestionnaireAdapterItem,
439+
newItem as QuestionnaireAdapterItem,
440+
)
441+
}
393442
}

datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ class QuestionnaireFragment : Fragment() {
163163
// Set items
164164
questionnaireEditRecyclerView.visibility = View.GONE
165165
questionnaireReviewAdapter.submitList(
166-
state.items,
166+
state.items.filterIsInstance<ReviewAdapterItem>(),
167167
)
168168
questionnaireReviewRecyclerView.visibility = View.VISIBLE
169169
reviewModeEditButton.visibility =

0 commit comments

Comments
 (0)