From 02ac69e9ad3079e14afe316fa799ed4432d31b55 Mon Sep 17 00:00:00 2001 From: Santosh Pingle Date: Thu, 6 Feb 2025 11:36:53 +0530 Subject: [PATCH 1/7] Clear the fields on create button is clicked. --- .../java/com/google/android/fhir/demo/CrudOperationFragment.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/demo/src/main/java/com/google/android/fhir/demo/CrudOperationFragment.kt b/demo/src/main/java/com/google/android/fhir/demo/CrudOperationFragment.kt index 2694044bc8..1c52c3d9cb 100644 --- a/demo/src/main/java/com/google/android/fhir/demo/CrudOperationFragment.kt +++ b/demo/src/main/java/com/google/android/fhir/demo/CrudOperationFragment.kt @@ -304,6 +304,7 @@ class CrudOperationFragment : Fragment() { gender = it.gender, isActive = it.isActive, ) + setupUiForCrudOperation(OperationType.CREATE) } } From d4cc9cf93458fbd1144bec7c209fe8debccf1e25 Mon Sep 17 00:00:00 2001 From: Santosh Pingle Date: Tue, 11 Feb 2025 15:58:19 +0530 Subject: [PATCH 2/7] multiple question in the same line. --- .../fhir/datacapture/QuestionnaireFragment.kt | 24 +++++- .../datacapture/QuestionnaireViewModel.kt | 5 ++ .../MoreQuestionnaireItemComponents.kt | 18 ++++- .../MoreQuestionnaireItemComponentsTest.kt | 73 ++++++++++++++++++- 4 files changed, 115 insertions(+), 5 deletions(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt index b84a625a10..e131d9576c 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt @@ -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. @@ -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 @@ -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 /** @@ -146,7 +148,7 @@ class QuestionnaireFragment : Fragment() { questionnaireEditRecyclerView.adapter = questionnaireEditAdapter val linearLayoutManager = LinearLayoutManager(view.context) - questionnaireEditRecyclerView.layoutManager = linearLayoutManager + questionnaireEditRecyclerView.layoutManager = getLayoutManager() // Animation does work well with views that could gain focus questionnaireEditRecyclerView.itemAnimator = null @@ -186,7 +188,9 @@ class QuestionnaireFragment : Fragment() { is DisplayMode.EditMode -> { // Set items questionnaireReviewRecyclerView.visibility = View.GONE - questionnaireEditAdapter.submitList(state.items) + questionnaireEditAdapter.submitList( + state.filterEmptyTextItems(), + ) // filterEmptyTextItems questionnaireEditRecyclerView.visibility = View.VISIBLE reviewModeEditButton.visibility = View.GONE @@ -584,6 +588,20 @@ class QuestionnaireFragment : Fragment() { QuestionnaireItemViewHolderFactoryMatchersProvider() { override fun get() = emptyList() } + + private fun getLayoutManager(): LinearLayoutManager { + return viewModel.columnCount?.let { GridLayoutManager(this.context, it) } + ?: LinearLayoutManager(this.context) + } + + internal fun QuestionnaireState.filterEmptyTextItems() = + items.filterNot { item -> + // Check if the item is a Question and has an empty text + // item is QuestionnaireAdapterItem.Question && + // item.item.questionnaireItem.text.isNullOrBlank() + item is QuestionnaireAdapterItem.Question && + item.item.questionnaireItem.type == QuestionnaireItemType.GROUP + } } /** diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt index e96bd28a50..8ee902aacd 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt @@ -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 @@ -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 { @@ -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. */ diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt index a29e0151e8..75a99d71be 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt @@ -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. @@ -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. // @@ -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, // diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponentsTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponentsTest.kt index beaf16d400..a2e1f3081c 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponentsTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponentsTest.kt @@ -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. @@ -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 From a3801b33ab80aca4ae6de32bdd7ffc6b23d679d2 Mon Sep 17 00:00:00 2001 From: Santosh Pingle Date: Tue, 11 Feb 2025 19:20:43 +0530 Subject: [PATCH 3/7] add check for gorup type item --- .../fhir/datacapture/QuestionnaireFragment.kt | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt index e131d9576c..030a58891b 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt @@ -188,9 +188,13 @@ class QuestionnaireFragment : Fragment() { is DisplayMode.EditMode -> { // Set items questionnaireReviewRecyclerView.visibility = View.GONE - questionnaireEditAdapter.submitList( - state.filterEmptyTextItems(), - ) // filterEmptyTextItems + val itemsToSubmit = + if (viewModel.columnCount != null) { + state.filterEmptyTextItems() + } else { + state.items + } + questionnaireEditAdapter.submitList(itemsToSubmit) questionnaireEditRecyclerView.visibility = View.VISIBLE reviewModeEditButton.visibility = View.GONE @@ -596,11 +600,9 @@ class QuestionnaireFragment : Fragment() { internal fun QuestionnaireState.filterEmptyTextItems() = items.filterNot { item -> - // Check if the item is a Question and has an empty text - // item is QuestionnaireAdapterItem.Question && - // item.item.questionnaireItem.text.isNullOrBlank() item is QuestionnaireAdapterItem.Question && - item.item.questionnaireItem.type == QuestionnaireItemType.GROUP + item.item.questionnaireItem.type == QuestionnaireItemType.GROUP && + item.item.questionText.isNullOrEmpty() } } From fee65e39fafcf1404a02ad2a725d29cc4f1b3a19 Mon Sep 17 00:00:00 2001 From: Santosh Pingle Date: Tue, 11 Feb 2025 19:20:43 +0530 Subject: [PATCH 4/7] add check for gorup type item --- .../java/com/google/android/fhir/demo/CrudOperationFragment.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/demo/src/main/java/com/google/android/fhir/demo/CrudOperationFragment.kt b/demo/src/main/java/com/google/android/fhir/demo/CrudOperationFragment.kt index 1c52c3d9cb..2694044bc8 100644 --- a/demo/src/main/java/com/google/android/fhir/demo/CrudOperationFragment.kt +++ b/demo/src/main/java/com/google/android/fhir/demo/CrudOperationFragment.kt @@ -304,7 +304,6 @@ class CrudOperationFragment : Fragment() { gender = it.gender, isActive = it.isActive, ) - setupUiForCrudOperation(OperationType.CREATE) } } From 28b2fdd98387a1a10d8ca8920c42528886145041 Mon Sep 17 00:00:00 2001 From: Santosh Pingle Date: Wed, 12 Feb 2025 15:47:28 +0530 Subject: [PATCH 5/7] fix test. --- .../android/fhir/datacapture/QuestionnaireFragment.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt index 030a58891b..3502285aed 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt @@ -147,8 +147,8 @@ class QuestionnaireFragment : Fragment() { } questionnaireEditRecyclerView.adapter = questionnaireEditAdapter - val linearLayoutManager = LinearLayoutManager(view.context) - questionnaireEditRecyclerView.layoutManager = getLayoutManager() + val linearLayoutManager = getLayoutManager() + questionnaireEditRecyclerView.layoutManager = linearLayoutManager // Animation does work well with views that could gain focus questionnaireEditRecyclerView.itemAnimator = null @@ -594,8 +594,11 @@ class QuestionnaireFragment : Fragment() { } private fun getLayoutManager(): LinearLayoutManager { - return viewModel.columnCount?.let { GridLayoutManager(this.context, it) } - ?: LinearLayoutManager(this.context) + return if (viewModel.columnCount != null) { + GridLayoutManager(context, viewModel.columnCount!!) + } else { + LinearLayoutManager(context) + } } internal fun QuestionnaireState.filterEmptyTextItems() = From 8ae69f8cf30632ee3d79595fefcb2abe06c10345 Mon Sep 17 00:00:00 2001 From: Santosh Pingle Date: Mon, 24 Feb 2025 14:15:45 +0530 Subject: [PATCH 6/7] respect column count per row. --- .../fhir/datacapture/QuestionnaireFragment.kt | 19 +++++++-- .../datacapture/QuestionnaireViewModel.kt | 13 +++++-- .../MoreQuestionnaireItemComponents.kt | 39 +++++++++++++++++++ .../extensions/MoreQuestionnaires.kt | 20 +++++++++- .../views/QuestionnaireViewItem.kt | 3 +- 5 files changed, 85 insertions(+), 9 deletions(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt index 3502285aed..10b29a0b9d 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt @@ -189,11 +189,24 @@ class QuestionnaireFragment : Fragment() { // Set items questionnaireReviewRecyclerView.visibility = View.GONE val itemsToSubmit = - if (viewModel.columnCount != null) { + if (viewModel.maxSpanSize != null) { state.filterEmptyTextItems() } else { state.items } + + (questionnaireEditRecyclerView.layoutManager as? GridLayoutManager)?.spanSizeLookup = + object : GridLayoutManager.SpanSizeLookup() { + override fun getSpanSize(position: Int): Int { + val item = itemsToSubmit[position] + return if (item is QuestionnaireAdapterItem.Question) { + item.item.spanSize ?: viewModel.maxSpanSize!! + } else { + viewModel.maxSpanSize!! + } + } + } + questionnaireEditAdapter.submitList(itemsToSubmit) questionnaireEditRecyclerView.visibility = View.VISIBLE reviewModeEditButton.visibility = View.GONE @@ -594,8 +607,8 @@ class QuestionnaireFragment : Fragment() { } private fun getLayoutManager(): LinearLayoutManager { - return if (viewModel.columnCount != null) { - GridLayoutManager(context, viewModel.columnCount!!) + return if (viewModel.maxSpanSize != null) { + GridLayoutManager(context, viewModel.maxSpanSize!!) } else { LinearLayoutManager(context) } diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt index 8ee902aacd..acb73352df 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt @@ -29,6 +29,7 @@ import com.google.android.fhir.datacapture.enablement.EnablementEvaluator import com.google.android.fhir.datacapture.expressions.EnabledAnswerOptionsEvaluator import com.google.android.fhir.datacapture.extensions.EntryMode import com.google.android.fhir.datacapture.extensions.allItems +import com.google.android.fhir.datacapture.extensions.calculateLCMOfColumnCounts import com.google.android.fhir.datacapture.extensions.calculatedExpression import com.google.android.fhir.datacapture.extensions.copyNestedItemsToChildlessAnswers import com.google.android.fhir.datacapture.extensions.cqfExpression @@ -36,6 +37,7 @@ import com.google.android.fhir.datacapture.extensions.createQuestionnaireRespons import com.google.android.fhir.datacapture.extensions.entryMode import com.google.android.fhir.datacapture.extensions.filterByCodeInNameExtension import com.google.android.fhir.datacapture.extensions.forEachItemPair +import com.google.android.fhir.datacapture.extensions.groupColumnSpanSizeMap import com.google.android.fhir.datacapture.extensions.hasDifferentAnswerSet import com.google.android.fhir.datacapture.extensions.isDisplayItem import com.google.android.fhir.datacapture.extensions.isHelpCode @@ -50,7 +52,6 @@ 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 @@ -100,7 +101,8 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat /** The current questionnaire as questions are being answered. */ internal val questionnaire: Questionnaire - internal val columnCount: Int? + internal var maxSpanSize: Int? = null + private var spanSizeMap: MutableMap? = null init { questionnaire = @@ -125,8 +127,6 @@ 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. */ @@ -171,6 +171,10 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat // Add extension for questionnaire launch time stamp questionnaireResponse.launchTimestamp = DateTimeType(Date()) questionnaireResponse.packRepeatedGroups(questionnaire) + questionnaire.calculateLCMOfColumnCounts()?.let { lcmValue -> + maxSpanSize = lcmValue + spanSizeMap = questionnaire.groupColumnSpanSizeMap(lcmValue) + } } /** @@ -983,6 +987,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat ), isHelpCardOpen = isHelpCard && isHelpCardOpen, helpCardStateChangedCallback = helpCardStateChangedCallback, + spanSize = spanSizeMap?.let { it.get(questionnaireItem) }, ), ) add(question) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt index 75a99d71be..51f0606175 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt @@ -43,6 +43,7 @@ import org.hl7.fhir.r4.model.Coding import org.hl7.fhir.r4.model.DecimalType import org.hl7.fhir.r4.model.Expression import org.hl7.fhir.r4.model.IntegerType +import org.hl7.fhir.r4.model.PositiveIntType import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent import org.hl7.fhir.r4.model.QuestionnaireResponse @@ -1066,3 +1067,41 @@ internal fun QuestionnaireItemComponent.readCustomStyleExtension(styleUrl: Style } return null } + +internal fun QuestionnaireItemComponent.getColumnCount(): Int? { + if (this.type != Questionnaire.QuestionnaireItemType.GROUP) return null + + return this.extension + .firstOrNull { it.url == EXTENSION_COLUMN_COUNT_URL } + ?.let { it.value as? PositiveIntType } + ?.value +} + +internal fun Questionnaire.groupColumnSpanSizeMap( + maxSpanSize: Int, +): MutableMap { + val spanSizeMap = mutableMapOf() + this.item + .filter { it.type == Questionnaire.QuestionnaireItemType.GROUP } + .forEach { groupItem -> + val columnCount = groupItem.getColumnCount() + if (columnCount != null) { + groupItem.processGroupItems(columnCount, maxSpanSize, spanSizeMap) + } + } + return spanSizeMap +} + +private fun QuestionnaireItemComponent.processGroupItems( + columnCount: Int, + maxSpanSize: Int, + spanSizeMap: MutableMap, +) { + this.item.forEach { childItem -> + if (childItem.type != Questionnaire.QuestionnaireItemType.GROUP) { + spanSizeMap[childItem] = maxSpanSize / columnCount + } else { + childItem.processGroupItems(columnCount, maxSpanSize, spanSizeMap) + } + } +} diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt index 6c0cc0c0ea..02320452f0 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt @@ -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. @@ -16,6 +16,7 @@ package com.google.android.fhir.datacapture.extensions +import kotlin.math.abs import org.hl7.fhir.exceptions.FHIRException import org.hl7.fhir.r4.model.CanonicalType import org.hl7.fhir.r4.model.CodeType @@ -228,3 +229,20 @@ private suspend fun forEachItemPair( } } } + +internal fun Questionnaire.calculateLCMOfColumnCounts(): Int? { + val columnCounts = this.item.mapNotNull { it.getColumnCount() } + return if (columnCounts.isNotEmpty()) { + columnCounts.reduce { acc, value -> lcm(acc, value) } + } else { + null + } +} + +private fun lcm(a: Int, b: Int): Int { + return abs(a * b) / gcd(a, b) +} + +private fun gcd(a: Int, b: Int): Int { + return if (b == 0) a else gcd(b, a % b) +} diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireViewItem.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireViewItem.kt index d9025fd284..12be0a7bdc 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireViewItem.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireViewItem.kt @@ -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. @@ -93,6 +93,7 @@ data class QuestionnaireViewItem( val helpCardStateChangedCallback: (Boolean, QuestionnaireResponseItemComponent) -> Unit = { _, _ -> }, + val spanSize: Int? = null, ) { fun getQuestionnaireResponseItem(): QuestionnaireResponseItemComponent = questionnaireResponseItem From 64c1642933267e9ec5b8426e46187509d144b65e Mon Sep 17 00:00:00 2001 From: Santosh Pingle Date: Mon, 24 Feb 2025 16:25:22 +0530 Subject: [PATCH 7/7] update tests. --- .../MoreQuestionnaireItemComponents.kt | 14 ---- .../MoreQuestionnaireItemComponentsTest.kt | 80 +++++++------------ 2 files changed, 31 insertions(+), 63 deletions(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt index 51f0606175..58f2fded9e 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt @@ -349,20 +349,6 @@ 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, // diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponentsTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponentsTest.kt index a2e1f3081c..a7fe98c45b 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponentsTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponentsTest.kt @@ -37,6 +37,7 @@ import org.hl7.fhir.r4.model.Expression import org.hl7.fhir.r4.model.Extension import org.hl7.fhir.r4.model.IntegerType import org.hl7.fhir.r4.model.Patient +import org.hl7.fhir.r4.model.PositiveIntType import org.hl7.fhir.r4.model.Quantity import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse @@ -2596,74 +2597,55 @@ class MoreQuestionnaireItemComponentsTest { } @Test - fun `rootGroupItemColumnCount returns correct column count`() { - val questionnaire = - Questionnaire().apply { - item = + fun `groupItemColumnCount returns correct column count`() { + val questionnaireItemComponent = + Questionnaire.QuestionnaireItemComponent().apply { + type = Questionnaire.QuestionnaireItemType.GROUP + extension = mutableListOf( - Questionnaire.QuestionnaireItemComponent().apply { - type = Questionnaire.QuestionnaireItemType.GROUP - extension = - mutableListOf( - Extension().apply { - url = EXTENSION_COLUMN_COUNT_URL - setValue(IntegerType(3)) - }, - ) + Extension().apply { + url = EXTENSION_COLUMN_COUNT_URL + setValue(PositiveIntType(3)) }, ) } - assertThat(questionnaire.rootGroupItemColumnCount).isEqualTo(3) + + assertThat(questionnaireItemComponent.getColumnCount()).isEqualTo(3) } @Test - fun `rootGroupItemColumnCount returns null when column count extension value is not an IntegerType`() { - val questionnaire = - Questionnaire().apply { - item = + fun `groupItemColumnCount returns null when column count extension value is not an PositiveIntType`() { + val questionnaireItemComponent = + Questionnaire.QuestionnaireItemComponent().apply { + type = Questionnaire.QuestionnaireItemType.GROUP + extension = mutableListOf( - Questionnaire.QuestionnaireItemComponent().apply { - type = Questionnaire.QuestionnaireItemType.GROUP - extension = - mutableListOf( - Extension().apply { - url = EXTENSION_COLUMN_COUNT_URL - setValue(StringType("invalid")) - }, - ) + Extension().apply { + url = EXTENSION_COLUMN_COUNT_URL + setValue(StringType("invalid")) }, ) } - assertThat(questionnaire.rootGroupItemColumnCount).isNull() + assertThat(questionnaireItemComponent.getColumnCount()).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() - }, - ) + fun `groupItemColumnCount returns null when GROUP item has no relevant extension`() { + val questionnaireItemComponent = + Questionnaire.QuestionnaireItemComponent().apply { + type = Questionnaire.QuestionnaireItemType.GROUP + extension = mutableListOf() } - assertThat(questionnaire.rootGroupItemColumnCount).isNull() + assertThat(questionnaireItemComponent.getColumnCount()).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 - }, - ) + fun `groupItemColumnCount returns null when no GROUP type item exists`() { + val questionnaireItemComponent = + Questionnaire.QuestionnaireItemComponent().apply { + type = Questionnaire.QuestionnaireItemType.BOOLEAN } - assertThat(questionnaire.rootGroupItemColumnCount).isNull() + assertThat(questionnaireItemComponent.getColumnCount()).isNull() } private val displayCategoryExtensionWithInstructionsCode =