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

Highlight missing fields and Show validation popup. #1671

Merged
merged 29 commits into from
Dec 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
219cc76
Show validation popup for missing fields
aditya-07 Oct 17, 2022
8914ba4
Merge branch 'master' into ak/highlight_required_fields
aditya-07 Oct 18, 2022
f370866
Fixed formatting
aditya-07 Oct 18, 2022
8eedb30
Merge branch 'master' into ak/highlight_required_fields
aditya-07 Oct 20, 2022
d174025
Review comments fix
aditya-07 Oct 28, 2022
96804f8
Merge branch 'master' into ak/highlight_required_fields
aditya-07 Oct 28, 2022
4982a4a
Merge branch 'master' into ak/highlight_required_fields
omarismail94 Nov 2, 2022
2ff3b8f
Merge branch 'master' into ak/highlight_required_fields
aditya-07 Nov 3, 2022
a086873
Merge branch 'master' into ak/highlight_required_fields
aditya-07 Nov 4, 2022
90abcb5
Merge branch 'master' into ak/highlight_required_fields
aditya-07 Nov 9, 2022
8b0bda4
Merge branch 'master' into ak/highlight_required_fields
aditya-07 Nov 16, 2022
3087e4a
Added companion object to ErrorMessage dialog fragment
aditya-07 Nov 16, 2022
e3bd8b2
Merge branch 'master' into ak/highlight_required_fields
aditya-07 Nov 18, 2022
e1eee19
Merge branch 'master' into ak/highlight_required_fields
aditya-07 Nov 24, 2022
3d59989
Review changes
aditya-07 Nov 28, 2022
7809af0
Merge branch 'master' into ak/highlight_required_fields
aditya-07 Dec 1, 2022
6d54060
Review Comments: Removed recycler view from popup.
aditya-07 Dec 2, 2022
f146dd9
Merge branch 'master' into ak/highlight_required_fields
aditya-07 Dec 2, 2022
2e9d889
Updated margin for errors
aditya-07 Dec 2, 2022
9f59b1d
Added scrollview for the dialog body.
aditya-07 Dec 2, 2022
91dec20
Update text
jingtang10 Dec 2, 2022
8c42962
Remove unnecessary import
jingtang10 Dec 2, 2022
b9c7d62
Merge branch 'master' into ak/highlight_required_fields
jingtang10 Dec 2, 2022
025f44c
Fix imports
jingtang10 Dec 2, 2022
d7ea140
Merge branch 'master' into ak/highlight_required_fields
jingtang10 Dec 4, 2022
fed6492
Fix errors
jingtang10 Dec 4, 2022
3122a3f
Merge branch 'ak/highlight_required_fields' of https://github.com/adi…
jingtang10 Dec 4, 2022
88096b5
Run spotless apply
jingtang10 Dec 4, 2022
f10166c
Fix missing import
jingtang10 Dec 4, 2022
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
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 {
aditya-07 marked this conversation as resolved.
Show resolved Hide resolved
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
}
aditya-07 marked this conversation as resolved.
Show resolved Hide resolved

/** @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
aditya-07 marked this conversation as resolved.
Show resolved Hide resolved
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