Skip to content

Commit

Permalink
Highlight missing fields and Show validation popup. (#1671)
Browse files Browse the repository at this point in the history
* Show validation popup for missing fields

* Fixed formatting

* Review comments fix

* Added companion object to ErrorMessage dialog fragment

* Review changes

* Review Comments: Removed recycler view from popup.

* Updated margin for errors

* Added scrollview for the dialog body.

* Update text

Co-authored-by: Omar Ismail <[email protected]>
Co-authored-by: Jing Tang <[email protected]>
  • Loading branch information
3 people committed Dec 4, 2022
1 parent cb13ade commit 9f3fc87
Show file tree
Hide file tree
Showing 10 changed files with 370 additions and 7 deletions.
1 change: 1 addition & 0 deletions catalog/src/main/assets/layout_default.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
{
"linkId": "2",
"type": "string",
"required": true,
"item": [
{
"linkId": "2.1",
Expand Down
10 changes: 8 additions & 2 deletions catalog/src/main/assets/layout_paginated.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
{
"linkId": "1.2",
"type": "string",
"required": true,
"item": [
{
"linkId": "1.2.1",
Expand Down Expand Up @@ -183,7 +184,8 @@
"linkId": "2.1",
"type": "date",
"text": "Date of birth",
"prefix": "2."
"prefix": "2.",
"required": true
},
{
"linkId": "2.2",
Expand Down Expand Up @@ -347,6 +349,7 @@
{
"linkId": "3.6",
"type": "string",
"required": true,
"item": [
{
"linkId": "14.1",
Expand Down Expand Up @@ -420,6 +423,7 @@
{
"linkId": "4.2",
"type": "string",
"required": true,
"item": [
{
"linkId": "16.1",
Expand All @@ -446,6 +450,7 @@
{
"linkId": "4.3",
"type": "choice",
"required": true,
"extension": [
{
"url": "http:https://hl7.org/fhir/StructureDefinition/questionnaire-itemControl",
Expand Down Expand Up @@ -847,7 +852,8 @@
"linkId": "10.1",
"type": "dateTime",
"text": "What was the date and time of the ultrasound?",
"prefix": "10."
"prefix": "10.",
"required": true
}
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,20 @@ import android.widget.Button
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.content.res.use
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.setFragmentResult
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.fhir.datacapture.validation.Invalid
import com.google.android.fhir.datacapture.views.QuestionnaireItemViewHolderFactory
import com.google.android.material.progressindicator.LinearProgressIndicator
import org.hl7.fhir.r4.model.Questionnaire

/**
* A [Fragment] for displaying FHIR Questionnaires and getting user responses as FHIR
* QuestionnareResponses.
* QuestionnaireResponses.
*
* For more information, see the
* [QuestionnaireFragment](https://github.com/google/android-fhir/wiki/SDCL%3A-Use-QuestionnaireFragment)
Expand Down Expand Up @@ -74,8 +76,20 @@ open class QuestionnaireFragment : Fragment() {
paginationPreviousButton.setOnClickListener { viewModel.goToPreviousPage() }
val paginationNextButton = view.findViewById<View>(R.id.pagination_next_button)
paginationNextButton.setOnClickListener { viewModel.goToNextPage() }
requireView().findViewById<Button>(R.id.submit_questionnaire).setOnClickListener {
setFragmentResult(SUBMIT_REQUEST_KEY, Bundle.EMPTY)
view.findViewById<Button>(R.id.submit_questionnaire).setOnClickListener {
viewModel.validateQuestionnaireAndUpdateUI().let { validationMap ->
if (validationMap.values.flatten().filterIsInstance<Invalid>().isEmpty()) {
setFragmentResult(SUBMIT_REQUEST_KEY, Bundle.EMPTY)
} else {
val errorViewModel: QuestionnaireValidationErrorViewModel by activityViewModels()
errorViewModel.setQuestionnaireAndValidation(viewModel.questionnaire, validationMap)
QuestionnaireValidationErrorMessageDialogFragment()
.show(
requireActivity().supportFragmentManager,
QuestionnaireValidationErrorMessageDialogFragment.TAG
)
}
}
}
val questionnaireProgressIndicator: LinearProgressIndicator =
view.findViewById(R.id.questionnaire_progress_indicator)
Expand Down Expand Up @@ -183,6 +197,10 @@ open class QuestionnaireFragment : Fragment() {
}
}
}
requireActivity().supportFragmentManager.setFragmentResultListener(
QuestionnaireValidationErrorMessageDialogFragment.RESULT_CALLBACK,
viewLifecycleOwner
) { _, _ -> setFragmentResult(SUBMIT_REQUEST_KEY, Bundle.EMPTY) }
}

/** Calculates the progress percentage from given [count] and [totalCount] values. */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Copyright 2022 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:https://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

import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import androidx.annotation.VisibleForTesting
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.content.res.use
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.setFragmentResult
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.google.android.fhir.datacapture.validation.Invalid
import com.google.android.fhir.datacapture.validation.ValidationResult
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.hl7.fhir.r4.model.Questionnaire

/**
* [Dialog] to highlight the required fields that need to be filled by the user before submitting
* the [Questionnaire]. This is shown when user has pressed internal submit button in the
* [QuestionnaireFragment] and there are some validation errors.
*/
internal class QuestionnaireValidationErrorMessageDialogFragment(
/**
* Factory helps with testing and should not be set to anything in the regular production flow.
*/
private val factoryProducer: (() -> ViewModelProvider.Factory)? = null
) : DialogFragment() {

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
isCancelable = false
return MaterialAlertDialogBuilder(requireContext())
.setView(onCreateCustomView())
.setPositiveButton(R.string.questionnaire_validation_error_fix_button_text) { dialog, _ ->
dialog?.dismiss()
}
.setNegativeButton(R.string.questionnaire_validation_error_submit_button_text) { dialog, _ ->
setFragmentResult(RESULT_CALLBACK, Bundle.EMPTY)
dialog?.dismiss()
}
.create()
}

@VisibleForTesting
fun onCreateCustomView(layoutInflater: LayoutInflater = getLayoutInflater()): View {
val themeId =
layoutInflater.context.obtainStyledAttributes(R.styleable.QuestionnaireTheme).use {
it.getResourceId(
// Use the custom questionnaire theme if it is specified
R.styleable.QuestionnaireTheme_questionnaire_theme,
// Otherwise, use the default questionnaire theme
R.style.Theme_Questionnaire
)
}

return layoutInflater
.cloneInContext(ContextThemeWrapper(layoutInflater.context, themeId))
.inflate(R.layout.questionnaire_validation_dialog, null)
.apply {
findViewById<TextView>(R.id.body).apply {
val viewModel: QuestionnaireValidationErrorViewModel by
activityViewModels(factoryProducer)
text =
viewModel.getItemsTextWithValidationErrors().joinToString(separator = "\n") {
context.getString(R.string.questionnaire_validation_error_item_text_with_bullet, it)
}
}
}
}

companion object {
const val TAG = "QuestionnaireValidationErrorMessageDialogFragment"
const val RESULT_CALLBACK = "QuestionnaireValidationResultCallback"
}
}

internal class QuestionnaireValidationErrorViewModel : ViewModel() {
private var questionnaire: Questionnaire? = null
private var validation: Map<String, List<ValidationResult>>? = null

fun setQuestionnaireAndValidation(
questionnaire: Questionnaire,
validation: Map<String, List<ValidationResult>>
) {
this.questionnaire = questionnaire
this.validation = validation
}

/** @return Texts associated with the failing [Questionnaire.QuestionnaireItemComponent]s. */
fun getItemsTextWithValidationErrors(): List<String> {
val invalidFields =
validation?.filterValues { it.filterIsInstance<Invalid>().isNotEmpty() } ?: emptyMap()
return questionnaire
?.item
?.flattened()
?.filter { invalidFields.contains(it.linkId) }
?.map { if (it.text.isNullOrEmpty()) it.localizedFlyoverSpanned.toString() else it.text }
?: emptyList()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,13 @@ import com.google.android.fhir.datacapture.enablement.EnablementEvaluator
import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator.detectExpressionCyclicDependency
import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator.evaluateCalculatedExpressions
import com.google.android.fhir.datacapture.utilities.fhirPathEngine
import com.google.android.fhir.datacapture.validation.Invalid
import com.google.android.fhir.datacapture.validation.NotValidated
import com.google.android.fhir.datacapture.validation.QuestionnaireResponseItemValidator
import com.google.android.fhir.datacapture.validation.QuestionnaireResponseValidator
import com.google.android.fhir.datacapture.validation.QuestionnaireResponseValidator.checkQuestionnaireResponse
import com.google.android.fhir.datacapture.validation.Valid
import com.google.android.fhir.datacapture.validation.ValidationResult
import com.google.android.fhir.datacapture.views.QuestionnaireItemViewItem
import com.google.android.fhir.search.search
import kotlinx.coroutines.flow.MutableStateFlow
Expand Down Expand Up @@ -97,6 +100,9 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
*/
private var isPaginationButtonPressed = false

/** Forces response validation each time [getQuestionnaireItemViewItems] is called. */
private var hasPressedSubmitButton = false

init {
when {
state.contains(QuestionnaireFragment.EXTRA_QUESTIONNAIRE_RESPONSE_JSON_URI) -> {
Expand Down Expand Up @@ -301,6 +307,23 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
}
}

/**
* Validates entire questionnaire and return the validation results. As a side effect, it triggers
* the UI update to show errors in case there are any validation errors.
*/
internal fun validateQuestionnaireAndUpdateUI(): Map<String, List<ValidationResult>> =
QuestionnaireResponseValidator.validateQuestionnaireResponse(
questionnaire,
questionnaireResponse,
getApplication()
)
.also { result ->
if (result.values.flatten().filterIsInstance<Invalid>().isNotEmpty()) {
hasPressedSubmitButton = true
modificationCount.update { it + 1 }
}
}

internal fun goToPreviousPage() {
when (entryMode) {
EntryMode.PRIOR_EDIT,
Expand Down Expand Up @@ -599,7 +622,8 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
// Determine the validation result, which will be displayed on the item itself
val validationResult =
if (modifiedQuestionnaireResponseItemSet.contains(questionnaireResponseItem) ||
isPaginationButtonPressed
isPaginationButtonPressed ||
hasPressedSubmitButton
) {
QuestionnaireResponseItemValidator.validate(
questionnaireItem,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
Copyright 2021 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:https://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.
-->
<LinearLayout
xmlns:android="http:https://schemas.android.com/apk/res/android"
xmlns:tools="http:https://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="@dimen/padding_modal_card"
>

<TextView
android:id="@+id/dialog_title"
style="?attr/questionnaireValidationDialogTitleStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Complete all required fields"
android:text="@string/questionnaire_validation_error_headline"
/>

<TextView
android:layout_marginTop="@dimen/padding_default"
android:id="@+id/dialog_subtitle"
style="?attr/questionnaireValidationDialogBodyStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Fields that need to be completed:"
android:text="@string/questionnaire_validation_error_supporting_text"
tools:visibility="visible"
/>

<ScrollView
android:layout_marginVertical="@dimen/item_margin_vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
<TextView
android:id="@+id/body"
style="?attr/questionnaireValidationDialogBodyStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Missing field."
tools:visibility="visible"
/>
</ScrollView>

</LinearLayout>
6 changes: 6 additions & 0 deletions datacapture/src/main/res/values/attrs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@
<attr name="questionnaireSubmitButtonStyle" format="reference" />
<!-- Style for edit button. -->
<attr name="questionnaireEditButtonStyle" format="reference" />
<!-- Style for validation error dialog title text in modal view. -->
<attr name="questionnaireValidationDialogTitleStyle" format="reference" />
<!-- Style for validation error dialog body text in modal view. -->
<attr name="questionnaireValidationDialogBodyStyle" format="reference" />
<!-- Style for validation error positive and negative dialog buttons in modal view. -->
<attr name="questionnaireValidationDialogButtonStyle" format="reference" />

<!-- Style for Linear Progress Indicator. -->
<attr name="questionnaireLinearProgressIndicatorStyle" format="reference" />
Expand Down
14 changes: 14 additions & 0 deletions datacapture/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,18 @@
<string name="space_asterisk">\u0020\u002a</string>
<string name="group_header_add_item_button">Add item</string>

<!-- Questionnaire validation popup texts-->
<string name="questionnaire_validation_error_headline">Errors found</string>
<string
name="questionnaire_validation_error_supporting_text"
>Fix the following questions:</string>
<string
name="questionnaire_validation_error_item_text_with_bullet"
>• %s</string>
<string
name="questionnaire_validation_error_submit_button_text"
>Submit anyway</string>
<string
name="questionnaire_validation_error_fix_button_text"
>Fix questions</string>
</resources>
Loading

0 comments on commit 9f3fc87

Please sign in to comment.