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
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,14 +611,20 @@ 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)).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,
1,
clickChildViewWithId(R.id.add_item),
),
)
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 Down Expand Up @@ -35,6 +35,10 @@ internal sealed interface QuestionnaireAdapterItem {
val title: String,
) : QuestionnaireAdapterItem

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

data class Navigation(val questionnaireNavigationUIState: QuestionnaireNavigationUIState) :
QuestionnaireAdapterItem
}
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.RepeatsGroupAddItemViewHolder
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(
RepeatsGroupAddItemViewHolder.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: RepeatsGroupAddItemViewHolder) :
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)
}
}
}

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 Down Expand Up @@ -39,6 +39,7 @@ internal class QuestionnaireReviewAdapter :
.inflate(R.layout.pagination_navigation_view, parent, false),
)
QuestionnaireEditAdapter.ViewType.Type.REPEATED_GROUP_HEADER -> TODO()
QuestionnaireEditAdapter.ViewType.Type.REPEATED_GROUP_ADD_BUTTON -> TODO()
}
}

Expand All @@ -53,6 +54,7 @@ internal class QuestionnaireReviewAdapter :
holder.bind(item.questionnaireNavigationUIState)
}
is QuestionnaireAdapterItem.RepeatedGroupHeader -> TODO()
is QuestionnaireAdapterItem.RepeatedGroupAddButton -> TODO()
}
}

Expand All @@ -74,6 +76,7 @@ internal class QuestionnaireReviewAdapter :
subtype = 0xFFFFFF
}
is QuestionnaireAdapterItem.RepeatedGroupHeader -> TODO()
is QuestionnaireAdapterItem.RepeatedGroupAddButton -> TODO()
}
return QuestionnaireEditAdapter.ViewType.from(type = type, subtype = subtype).viewType
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1024,6 +1024,10 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
),
)
}

if (questionnaireItem.isRepeatedGroup) {
add(QuestionnaireAdapterItem.RepeatedGroupAddButton(question.item))
}
}
currentPageItems = items
return items
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.android.fhir.datacapture.views

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import com.google.android.fhir.datacapture.R
import com.google.android.fhir.datacapture.extensions.tryUnwrapContext
import kotlinx.coroutines.launch
import org.hl7.fhir.r4.model.QuestionnaireResponse

class RepeatsGroupAddItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

private var context: AppCompatActivity = itemView.context.tryUnwrapContext()!!

fun bind(questionnaireViewItem: QuestionnaireViewItem) {
val addItemButton: Button = itemView.findViewById(R.id.add_item)

addItemButton.text =
itemView.context.getString(
R.string.add_repeated_group_item,
questionnaireViewItem.questionText ?: "",
)
addItemButton.visibility =
if (questionnaireViewItem.questionnaireItem.repeats) View.VISIBLE else View.GONE
addItemButton.setOnClickListener {
context.lifecycleScope.launch {
questionnaireViewItem.addAnswer(
// Nested items will be added in answerChangedCallback in the QuestionnaireViewModel
QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent(),
)
}
}

addItemButton.isEnabled = !questionnaireViewItem.questionnaireItem.readOnly
}

companion object {
val layoutRes = R.layout.add_repeated_item

fun create(parent: ViewGroup): RepeatsGroupAddItemViewHolder {
return RepeatsGroupAddItemViewHolder(
LayoutInflater.from(parent.context).inflate(layoutRes, parent, false),
)
}
}
}
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 @@ -17,10 +17,8 @@
package com.google.android.fhir.datacapture.views.factories

import android.view.View
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.google.android.fhir.datacapture.R
import com.google.android.fhir.datacapture.extensions.tryUnwrapContext
import com.google.android.fhir.datacapture.validation.Invalid
Expand All @@ -29,8 +27,6 @@ import com.google.android.fhir.datacapture.validation.Valid
import com.google.android.fhir.datacapture.validation.ValidationResult
import com.google.android.fhir.datacapture.views.GroupHeaderView
import com.google.android.fhir.datacapture.views.QuestionnaireViewItem
import kotlinx.coroutines.launch
import org.hl7.fhir.r4.model.QuestionnaireResponse

internal object GroupViewHolderFactory :
QuestionnaireItemViewHolderFactory(R.layout.group_header_view) {
Expand All @@ -39,33 +35,16 @@ internal object GroupViewHolderFactory :
private lateinit var context: AppCompatActivity
private lateinit var header: GroupHeaderView
private lateinit var error: TextView
private lateinit var addItemButton: Button
override lateinit var questionnaireViewItem: QuestionnaireViewItem

override fun init(itemView: View) {
context = itemView.context.tryUnwrapContext()!!
header = itemView.findViewById(R.id.header)
error = itemView.findViewById(R.id.error)
addItemButton = itemView.findViewById(R.id.add_item)
}

override fun bind(questionnaireViewItem: QuestionnaireViewItem) {
header.bind(questionnaireViewItem)
addItemButton.text =
context.getString(
R.string.add_repeated_group_item,
questionnaireViewItem.questionText ?: "",
)
addItemButton.visibility =
if (questionnaireViewItem.questionnaireItem.repeats) View.VISIBLE else View.GONE
addItemButton.setOnClickListener {
context.lifecycleScope.launch {
questionnaireViewItem.addAnswer(
// Nested items will be added in answerChangedCallback in the QuestionnaireViewModel
QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent(),
)
}
}
displayValidationResult(questionnaireViewItem.validationResult)
}

Expand All @@ -81,7 +60,7 @@ internal object GroupViewHolderFactory :
}

override fun setReadOnly(isReadOnly: Boolean) {
addItemButton.isEnabled = !isReadOnly
// No-op
}
}
}
17 changes: 17 additions & 0 deletions datacapture/src/main/res/layout/add_repeated_item.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8" ?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>

<com.google.android.material.button.MaterialButton
android:id="@+id/add_item"
style="?attr/questionnaireAddRepeatedGroupButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
/>

</LinearLayout>
8 changes: 0 additions & 8 deletions datacapture/src/main/res/layout/group_header_view.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,4 @@
android:layout_height="wrap_content"
/>

<com.google.android.material.button.MaterialButton
android:id="@+id/add_item"
style="?attr/questionnaireAddRepeatedGroupButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
/>

</LinearLayout>
Loading
Loading