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 10 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,23 @@ 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
.filter { (_, validations) -> validations.filterIsInstance<Invalid>().isNotEmpty() }
.isEmpty()
aditya-07 marked this conversation as resolved.
Show resolved Hide resolved
) {
setFragmentResult(SUBMIT_REQUEST_KEY, Bundle.EMPTY)
} else {
val errorViewModel: QuestionnaireValidationErrorViewModel by activityViewModels()
errorViewModel.setQuestionnaireAndValidation(viewModel.questionnaire, validationMap)
QuestionnaireValidationErrorMessageDialogFragment()
.show(
requireActivity().supportFragmentManager,
"QuestionnaireValidationErrorMessageDialogFragment"
aditya-07 marked this conversation as resolved.
Show resolved Hide resolved
)
}
}
}
val questionnaireProgressIndicator: LinearProgressIndicator =
view.findViewById(R.id.questionnaire_progress_indicator)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*
* 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.view.ViewGroup
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.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.google.android.fhir.datacapture.validation.Invalid
import com.google.android.fhir.datacapture.validation.ValidationResult
import com.google.android.fhir.datacapture.views.MarginItemDecoration
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()).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_error_dialog, null)
.apply {
findViewById<RecyclerView>(R.id.recycler_view).apply {
layoutManager = LinearLayoutManager(context)
addItemDecoration(
MarginItemDecoration(
context.resources.getDimensionPixelOffset(R.dimen.instructions_top_margin)
)
)
val viewModel: QuestionnaireValidationErrorViewModel by
activityViewModels(factoryProducer)
adapter =
ErrorAdapter().apply { submitList(viewModel.getItemsTextWithValidationErrors()) }
}
findViewById<View>(R.id.positive_button).setOnClickListener { dialog?.dismiss() }
}
}

class ErrorAdapter :
ListAdapter<ValidationErrorDataModel, ErrorViewHolder>(ValidationDiffCallback) {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
ErrorViewHolder(parent, R.layout.questionnaire_validation_dialog_item)

override fun onBindViewHolder(holder: ErrorViewHolder, position: Int) {
holder.bind(getItem(position))
}
}

class ErrorViewHolder(parent: ViewGroup, layout: Int) :
RecyclerView.ViewHolder(LayoutInflater.from(parent.context).inflate(layout, parent, false)) {

val textView: TextView = itemView.findViewById(R.id.text_view)

fun bind(data: ValidationErrorDataModel) {
textView.text =
itemView.context.getString(
R.string.questionnaire_validation_error_item_text_with_bullet,
data.questionnaireItemText
)
}
}
}

private val ValidationDiffCallback =
aditya-07 marked this conversation as resolved.
Show resolved Hide resolved
object : DiffUtil.ItemCallback<ValidationErrorDataModel>() {
override fun areItemsTheSame(
oldItem: ValidationErrorDataModel,
newItem: ValidationErrorDataModel
) = oldItem.linkId == newItem.linkId

override fun areContentsTheSame(
oldItem: ValidationErrorDataModel,
newItem: ValidationErrorDataModel
) = oldItem.questionnaireItemText == newItem.questionnaireItemText
}

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<ValidationErrorDataModel> {
val invalidFields =
validation?.filterValues { it.filterIsInstance<Invalid>().isNotEmpty() } ?: emptyMap()
return questionnaire
?.item
?.flattened()
?.filter { invalidFields.contains(it.linkId) }
?.map {
ValidationErrorDataModel(
it.linkId,
if (it.text.isNullOrEmpty()) it.localizedFlyoverSpanned.toString() else it.text
)
}
?: emptyList()
}
}

data class ValidationErrorDataModel(val linkId: String, val questionnaireItemText: String)
aditya-07 marked this conversation as resolved.
Show resolved Hide resolved
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.Flow
Expand Down Expand Up @@ -96,6 +99,10 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
* needed to avoid spewing validation errors before any questions are answered.
*/
private var isPaginationButtonPressed = false

/** Forces response validation each time [getQuestionnaireItemViewItems] is called. */
private var forceValidate = false
aditya-07 marked this conversation as resolved.
Show resolved Hide resolved

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

/**
* Validates questionnaire and return the validation results. As a side effect, it triggers the UI
aditya-07 marked this conversation as resolved.
Show resolved Hide resolved
* 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()) {
forceValidate = true
aditya-07 marked this conversation as resolved.
Show resolved Hide resolved
modificationCount.update { it + 1 }
}
}

internal fun goToPreviousPage() {
when (entryMode) {
EntryMode.PRIOR_EDIT,
Expand Down Expand Up @@ -618,7 +642,8 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat

val validationResult =
if (modifiedQuestionnaireResponseItemSet.contains(questionnaireResponseItem) ||
isPaginationButtonPressed
isPaginationButtonPressed ||
forceValidate
) {
QuestionnaireResponseItemValidator.validate(
questionnaireItem,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?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.
-->
<com.google.android.material.textview.MaterialTextView
aditya-07 marked this conversation as resolved.
Show resolved Hide resolved
xmlns:android="http:https://schemas.android.com/apk/res/android"
xmlns:tools="http:https://schemas.android.com/tools"
android:id="@+id/text_view"
style="?attr/questionnaireValidationDialogBodyStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Option"
/>
Loading