Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rendering multiple questions in the same line. #2789

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
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
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 @@ -30,6 +30,7 @@ import androidx.fragment.app.activityViewModels
import androidx.fragment.app.setFragmentResult
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.fhir.datacapture.validation.Invalid
Expand All @@ -38,6 +39,7 @@ import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemView
import com.google.android.material.progressindicator.LinearProgressIndicator
import kotlinx.coroutines.launch
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemType
import timber.log.Timber

/**
Expand Down Expand Up @@ -145,7 +147,7 @@ class QuestionnaireFragment : Fragment() {
}

questionnaireEditRecyclerView.adapter = questionnaireEditAdapter
val linearLayoutManager = LinearLayoutManager(view.context)
val linearLayoutManager = getLayoutManager()
questionnaireEditRecyclerView.layoutManager = linearLayoutManager
// Animation does work well with views that could gain focus
questionnaireEditRecyclerView.itemAnimator = null
Expand Down Expand Up @@ -186,7 +188,13 @@ class QuestionnaireFragment : Fragment() {
is DisplayMode.EditMode -> {
// Set items
questionnaireReviewRecyclerView.visibility = View.GONE
questionnaireEditAdapter.submitList(state.items)
val itemsToSubmit =
if (viewModel.columnCount != null) {
state.filterEmptyTextItems()
} else {
state.items
}
questionnaireEditAdapter.submitList(itemsToSubmit)
questionnaireEditRecyclerView.visibility = View.VISIBLE
reviewModeEditButton.visibility = View.GONE

Expand Down Expand Up @@ -584,6 +592,21 @@ class QuestionnaireFragment : Fragment() {
QuestionnaireItemViewHolderFactoryMatchersProvider() {
override fun get() = emptyList<QuestionnaireItemViewHolderFactoryMatcher>()
}

private fun getLayoutManager(): LinearLayoutManager {
return if (viewModel.columnCount != null) {
GridLayoutManager(context, viewModel.columnCount!!)
} else {
LinearLayoutManager(context)
}
}

internal fun QuestionnaireState.filterEmptyTextItems() =
items.filterNot { item ->
item is QuestionnaireAdapterItem.Question &&
item.item.questionnaireItem.type == QuestionnaireItemType.GROUP &&
item.item.questionText.isNullOrEmpty()
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import com.google.android.fhir.datacapture.extensions.minValue
import com.google.android.fhir.datacapture.extensions.minValueCqfCalculatedValueExpression
import com.google.android.fhir.datacapture.extensions.packRepeatedGroups
import com.google.android.fhir.datacapture.extensions.questionnaireLaunchContexts
import com.google.android.fhir.datacapture.extensions.rootGroupItemColumnCount
import com.google.android.fhir.datacapture.extensions.shouldHaveNestedItemsUnderAnswers
import com.google.android.fhir.datacapture.extensions.unpackRepeatedGroups
import com.google.android.fhir.datacapture.extensions.validateLaunchContextExtensions
Expand Down Expand Up @@ -99,6 +100,8 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
/** The current questionnaire as questions are being answered. */
internal val questionnaire: Questionnaire

internal val columnCount: Int?

init {
questionnaire =
when {
Expand All @@ -122,6 +125,8 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
"Neither EXTRA_QUESTIONNAIRE_JSON_URI nor EXTRA_QUESTIONNAIRE_JSON_STRING is supplied.",
)
}
columnCount = questionnaire.rootGroupItemColumnCount
Timber.d("Column count value is $columnCount")
}

/** The current questionnaire response as questions are being answered. */
Expand Down
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 @@ -177,6 +177,9 @@ internal const val EXTENSION_VARIABLE_URL = "http://hl7.org/fhir/StructureDefini
internal const val ITEM_INITIAL_EXPRESSION_URL: String =
"http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression"

internal const val EXTENSION_COLUMN_COUNT_URL: String =
"http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-columnCount"

// ********************************************************************************************** //
// //
// Rendering extensions: item control, choice orientation, etc. //
Expand Down Expand Up @@ -346,6 +349,19 @@ internal val QuestionnaireItemComponent.maxValue
internal val QuestionnaireItemComponent.maxValueCqfCalculatedValueExpression
get() = getExtensionByUrl(MAX_VALUE_EXTENSION_URL)?.value?.cqfCalculatedValueExpression

val Questionnaire.rootGroupItemColumnCount: Int?
get() {
val rootGroupItem =
this.item.firstOrNull { it.type == Questionnaire.QuestionnaireItemType.GROUP }

// Ensure the item exists and contains the relevant extension
val columnCountExtension =
rootGroupItem?.extension?.firstOrNull { it.url == EXTENSION_COLUMN_COUNT_URL }

// Extract and return the column count value if available
return (columnCountExtension?.value as? IntegerType)?.value
}

// ********************************************************************************************** //
// //
// Additional display utilities: display item control, localized text spanned, //
Expand Down
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 @@ -2595,6 +2595,77 @@ class MoreQuestionnaireItemComponentsTest {
assertThat(question.isRepeatedGroup).isFalse()
}

@Test
fun `rootGroupItemColumnCount returns correct column count`() {
val questionnaire =
Questionnaire().apply {
item =
mutableListOf(
Questionnaire.QuestionnaireItemComponent().apply {
type = Questionnaire.QuestionnaireItemType.GROUP
extension =
mutableListOf(
Extension().apply {
url = EXTENSION_COLUMN_COUNT_URL
setValue(IntegerType(3))
},
)
},
)
}
assertThat(questionnaire.rootGroupItemColumnCount).isEqualTo(3)
}

@Test
fun `rootGroupItemColumnCount returns null when column count extension value is not an IntegerType`() {
val questionnaire =
Questionnaire().apply {
item =
mutableListOf(
Questionnaire.QuestionnaireItemComponent().apply {
type = Questionnaire.QuestionnaireItemType.GROUP
extension =
mutableListOf(
Extension().apply {
url = EXTENSION_COLUMN_COUNT_URL
setValue(StringType("invalid"))
},
)
},
)
}
assertThat(questionnaire.rootGroupItemColumnCount).isNull()
}

@Test
fun `rootGroupItemColumnCount returns null when GROUP item has no relevant extension`() {
val questionnaire =
Questionnaire().apply {
item =
mutableListOf(
Questionnaire.QuestionnaireItemComponent().apply {
type = Questionnaire.QuestionnaireItemType.GROUP
extension = mutableListOf()
},
)
}
assertThat(questionnaire.rootGroupItemColumnCount).isNull()
}

@Test
fun `rootGroupItemColumnCount returns null when no GROUP type item exists`() {
val questionnaire =
Questionnaire().apply {
item =
mutableListOf(
Questionnaire.QuestionnaireItemComponent().apply {
type = Questionnaire.QuestionnaireItemType.BOOLEAN
},
)
}
assertThat(questionnaire.rootGroupItemColumnCount).isNull()
}

private val displayCategoryExtensionWithInstructionsCode =
Extension().apply {
url = EXTENSION_DISPLAY_CATEGORY_URL
Expand Down
Loading