From 5b791775b6cdcba5b155c88cf4c8ea754a1604a6 Mon Sep 17 00:00:00 2001 From: Hanny Prastya Hariyadi Date: Mon, 2 Oct 2023 16:18:59 +0900 Subject: [PATCH 01/23] Remove locale and lookupFDashLocalizations in view layer --- lib/extensions/function_extension.dart | 26 ++++++ lib/fhir_types/src/date_time_picker.dart | 12 +-- .../model/aggregation/src/aggregator.dart | 8 +- .../aggregation/src/narrative_aggregator.dart | 12 ++- .../questionnaire_response_aggregator.dart | 9 +- .../src/total_score_aggregator.dart | 10 ++- .../model/item/answer/src/answer_model.dart | 54 ++++++------ .../answer/src/attachment_answer_model.dart | 29 ++++--- .../item/answer/src/boolean_answer_model.dart | 4 +- .../item/answer/src/coding_answer_model.dart | 38 ++++---- .../src/coding_answer_option_model.dart | 3 +- .../answer/src/datetime_answer_model.dart | 27 +++--- .../answer/src/numerical_answer_model.dart | 47 +++++----- .../item/answer/src/string_answer_model.dart | 79 +++++++++-------- .../answer/src/unsupported_answer_model.dart | 4 +- .../model/item/src/question_item_model.dart | 36 +++----- .../model/item/src/response_item_model.dart | 69 ++++++++------- .../src/questionnaire_response_model.dart | 33 +++---- .../model/src/rendering_string.dart | 4 + .../answer/src/attachment_answer_filler.dart | 6 +- .../answer/src/boolean_answer_filler.dart | 14 +-- .../item/answer/src/coding_answer_filler.dart | 32 ++++--- .../answer/src/datetime_answer_filler.dart | 3 +- .../answer/src/numerical_answer_filler.dart | 37 +++++--- .../item/answer/src/string_answer_filler.dart | 17 +++- .../view/item/src/group_item.dart | 5 +- .../src/question_response_item_filler.dart | 8 +- .../item/src/questionnaire_item_filler.dart | 11 ++- .../src/questionnaire_item_filler_title.dart | 10 ++- .../src/questionnaire_complete_button.dart | 5 +- .../view/src/questionnaire_filler.dart | 60 ++++++------- .../view/src/questionnaire_page_scaffold.dart | 3 +- .../view/src/questionnaire_scroller.dart | 87 +++++++++---------- .../view/src/questionnaire_scroller_page.dart | 3 - .../view/src/questionnaire_stepper.dart | 6 +- .../view/src/questionnaire_stepper_page.dart | 2 - .../src/questionnaire_stepper_page_view.dart | 21 +++-- .../view/src/questionnaire_theme.dart | 34 ++++---- lib/questionnaires/view/src/xhtml.dart | 11 ++- 39 files changed, 494 insertions(+), 385 deletions(-) create mode 100644 lib/extensions/function_extension.dart diff --git a/lib/extensions/function_extension.dart b/lib/extensions/function_extension.dart new file mode 100644 index 00000000..656c0428 --- /dev/null +++ b/lib/extensions/function_extension.dart @@ -0,0 +1,26 @@ +/// An extension on [Function] that provides a method to call functions +/// safely, which means if the function throws an error, it will catch that +/// error and return `null`, instead of letting the error propagate. +/// +/// This can be useful when working with functions that may throw exceptions +/// but you don't want to handle those exceptions explicitly or when you +/// prefer to deal with a `null` return value in the case of an error. +extension TryOrNull on Function { + /// Calls the function safely and returns the result of the function if it's + /// successful, or `null` if an error occurs. + /// + /// Here, `T` represents the expected return type of the function. + /// + /// Example: + /// ```dart + /// Function riskyFunction = () => throw Exception('Oops!'); + /// final result = riskyFunction.callSafely(); // result will be `null`. + /// ``` + T? callSafely() { + try { + return this.call() as T; + } catch (e) { + return null; + } + } +} diff --git a/lib/fhir_types/src/date_time_picker.dart b/lib/fhir_types/src/date_time_picker.dart index f1bf5769..23b0ad0e 100644 --- a/lib/fhir_types/src/date_time_picker.dart +++ b/lib/fhir_types/src/date_time_picker.dart @@ -76,8 +76,8 @@ class _FhirDateTimePickerState extends State { if (value == null || dateTime == null) return ''; return (widget.pickerType == Time) - ? DateFormat.jm(locale.toString()).format(dateTime) - : value.format(locale, withTimeZone: widget.pickerType == FhirDateTime); + ? DateFormat.jm(locale.toString()).format(dateTime) + : value.format(locale, withTimeZone: widget.pickerType == FhirDateTime); } Future _showPicker(Locale locale) async { @@ -113,13 +113,15 @@ class _FhirDateTimePickerState extends State { // Get new BuildContext with overridden locale builder: (context) { // Get time of day format of current locale - final timeOfDayFormat = MaterialLocalizations.of(context).timeOfDayFormat(); + final timeOfDayFormat = + MaterialLocalizations.of(context).timeOfDayFormat(); return MediaQuery( data: MediaQuery.of(context).copyWith( // Workaround for time picker validation bug in `input` mode with locales specifying a 24h TimeOfDayFormat. // - https://github.com/sujrd/faiadashu/pull/32#issuecomment-1678639964 // - https://github.com/flutter/flutter/issues/85527 - alwaysUse24HourFormat: timeOfDay24hFormats.contains(timeOfDayFormat), + alwaysUse24HourFormat: + timeOfDay24hFormats.contains(timeOfDayFormat), ), child: child!, ); @@ -157,7 +159,7 @@ class _FhirDateTimePickerState extends State { @override Widget build(BuildContext context) { - final locale = widget.locale ?? Localizations.localeOf(context); + final locale = Localizations.localeOf(context); // There is no Locale in initState. if (!_fieldInitialized) { diff --git a/lib/questionnaires/model/aggregation/src/aggregator.dart b/lib/questionnaires/model/aggregation/src/aggregator.dart index ac756372..d62cf3c1 100644 --- a/lib/questionnaires/model/aggregation/src/aggregator.dart +++ b/lib/questionnaires/model/aggregation/src/aggregator.dart @@ -1,3 +1,4 @@ +import 'package:faiadashu/l10n/l10n.dart'; import 'package:faiadashu/questionnaires/model/model.dart'; import 'package:flutter/material.dart'; @@ -8,11 +9,16 @@ import 'package:flutter/material.dart'; abstract class Aggregator extends ValueNotifier { late final QuestionnaireResponseModel questionnaireResponseModel; late final Locale locale; + final FDashLocalizations localizations; final bool autoAggregate; /// [autoAggregate] specifies whether it should attach listeners to the /// questionnaire and aggregate when the questionnaire changes. - Aggregator(T initialValue, {this.autoAggregate = true}) : super(initialValue); + Aggregator( + T initialValue, { + required this.localizations, + this.autoAggregate = true, + }) : super(initialValue); // ignore: use_setters_to_change_properties /// Initialize the aggregator. diff --git a/lib/questionnaires/model/aggregation/src/narrative_aggregator.dart b/lib/questionnaires/model/aggregation/src/narrative_aggregator.dart index 821cf9c4..c392cc6a 100644 --- a/lib/questionnaires/model/aggregation/src/narrative_aggregator.dart +++ b/lib/questionnaires/model/aggregation/src/narrative_aggregator.dart @@ -18,8 +18,12 @@ class NarrativeAggregator extends Aggregator { status: NarrativeStatus.empty, ); - NarrativeAggregator() - : super(NarrativeAggregator.emptyNarrative, autoAggregate: false); + NarrativeAggregator({required FDashLocalizations localizations}) + : super( + NarrativeAggregator.emptyNarrative, + localizations: localizations, + autoAggregate: false, + ); @override void init(QuestionnaireResponseModel questionnaireResponseModel) { @@ -121,7 +125,7 @@ class NarrativeAggregator extends Aggregator { if (invalid) { div.write( - '${lookupFDashLocalizations(locale).dataAbsentReasonAsTextOutput} ', + '${localizations.dataAbsentReasonAsTextOutput} ', ); } @@ -129,7 +133,7 @@ class NarrativeAggregator extends Aggregator { div.write('

***

'); } else if (dataAbsentReason == dataAbsentReasonAskedButDeclinedCode) { div.write( - '

X ${lookupFDashLocalizations(locale).dataAbsentReasonAskedDeclinedOutput}

', + '

X ${localizations.dataAbsentReasonAskedDeclinedOutput}

', ); } else { final filledAnswers = itemModel.answeredAnswerModels; diff --git a/lib/questionnaires/model/aggregation/src/questionnaire_response_aggregator.dart b/lib/questionnaires/model/aggregation/src/questionnaire_response_aggregator.dart index a5af7688..ea0c811c 100644 --- a/lib/questionnaires/model/aggregation/src/questionnaire_response_aggregator.dart +++ b/lib/questionnaires/model/aggregation/src/questionnaire_response_aggregator.dart @@ -1,5 +1,6 @@ import 'package:faiadashu/coding/coding.dart'; import 'package:faiadashu/fhir_types/fhir_types.dart'; +import 'package:faiadashu/l10n/l10n.dart'; import 'package:faiadashu/logging/logging.dart'; import 'package:faiadashu/questionnaires/questionnaires.dart'; import 'package:fhir/r4.dart'; @@ -13,8 +14,12 @@ class QuestionnaireResponseAggregator extends Aggregator { static final Logger _logger = Logger(QuestionnaireResponseAggregator); - QuestionnaireResponseAggregator() - : super(QuestionnaireResponse(), autoAggregate: false); + QuestionnaireResponseAggregator({required FDashLocalizations localizations}) + : super( + QuestionnaireResponse(), + localizations: localizations, + autoAggregate: false, + ); QuestionnaireResponseItem? _fromQuestionItem( QuestionItemModel itemModel, diff --git a/lib/questionnaires/model/aggregation/src/total_score_aggregator.dart b/lib/questionnaires/model/aggregation/src/total_score_aggregator.dart index 313929e2..a1cec24d 100644 --- a/lib/questionnaires/model/aggregation/src/total_score_aggregator.dart +++ b/lib/questionnaires/model/aggregation/src/total_score_aggregator.dart @@ -1,4 +1,5 @@ import 'package:collection/collection.dart'; +import 'package:faiadashu/l10n/l10n.dart'; import 'package:faiadashu/logging/logging.dart'; import 'package:faiadashu/questionnaires/model/model.dart'; import 'package:fhir/r4.dart'; @@ -16,8 +17,13 @@ class TotalScoreAggregator extends Aggregator { static final _logger = Logger(TotalScoreAggregator); late final QuestionItemModel? totalScoreItem; - TotalScoreAggregator({bool autoAggregate = true}) - : super(Decimal(0), autoAggregate: autoAggregate); + TotalScoreAggregator( + {required FDashLocalizations localizations, bool autoAggregate = true}) + : super( + Decimal(0), + localizations: localizations, + autoAggregate: autoAggregate, + ); @override void init(QuestionnaireResponseModel questionnaireResponseModel) { diff --git a/lib/questionnaires/model/item/answer/src/answer_model.dart b/lib/questionnaires/model/item/answer/src/answer_model.dart index 70312201..8d9198d0 100644 --- a/lib/questionnaires/model/item/answer/src/answer_model.dart +++ b/lib/questionnaires/model/item/answer/src/answer_model.dart @@ -1,5 +1,5 @@ -import 'package:faiadashu/fhir_types/fhir_types.dart'; -import 'package:faiadashu/questionnaires/model/model.dart'; +import 'package:faiadashu/faiadashu.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; import 'package:fhir/r4.dart'; import 'package:flutter/material.dart'; @@ -74,16 +74,16 @@ abstract class AnswerModel extends ResponseNode { /// Validates a new input value. Does not change the [value]. /// - /// Returns null when [inputValue] is valid, or a localized message when it is not. - /// /// This is used to validate external input from a view. - String? validateInput(I? inputValue); + /// + /// Throws [ValidationError] when when [inputValue] is invalid. + void validateInput(I? inputValue); /// Validates a value against the constraints of the answer model. /// Does not change the [value] of the answer model. /// - /// Returns null when it is valid, or a localized message when it is not. - String? validateValue(V? inputValue); + /// Throws [ValidationError] when when [inputValue] is invalid. + void validateValue(V? inputValue); /// Validates whether the current [value] will pass the completeness check. /// @@ -93,29 +93,25 @@ abstract class AnswerModel extends ResponseNode { /// Since an individual answer does not know whether it is required, this /// is not taken into account. /// - /// Returns null when the answer is valid, or an error text, - /// when it is not. - /// - String? validate({ + /// Returns an empty list when the answer is valid, otherwise + /// Returns a list of [ValidationError]. + List validate({ bool updateErrorText = true, bool notifyListeners = false, }) { - final newErrorText = validateValue( - value, - ); - - if (errorText == newErrorText) { - return newErrorText; + try { + validateValue(value); + + if (notifyListeners) { + this.notifyListeners(); + } + return []; + } on ValidationError catch (exception) { + if (updateErrorText) { + _error = exception; + } + return [exception]; } - - if (updateErrorText) { - errorText = newErrorText; - } - if (notifyListeners) { - this.notifyListeners(); - } - - return newErrorText; } /// Returns whether any answer (valid or invalid) has been provided. @@ -124,12 +120,14 @@ abstract class AnswerModel extends ResponseNode { /// Returns whether this question is unanswered. bool get isEmpty; - String? errorText; + ValidationError? _error; /// Returns an error text for display in the answer's control. /// /// This might return an error text from the parent [QuestionItemModel]. - String? get displayErrorText => errorText ?? responseItemModel.errorText; + String? displayErrorText(FDashLocalizations localizations) => + _error?.getMessage(localizations) ?? + responseItemModel.getErrorText(localizations); /// Returns a [QuestionnaireResponseAnswer] based on the current value. /// diff --git a/lib/questionnaires/model/item/answer/src/attachment_answer_model.dart b/lib/questionnaires/model/item/answer/src/attachment_answer_model.dart index ffeab84e..abdeae1f 100644 --- a/lib/questionnaires/model/item/answer/src/attachment_answer_model.dart +++ b/lib/questionnaires/model/item/answer/src/attachment_answer_model.dart @@ -1,8 +1,8 @@ import 'package:faiadashu/fhir_types/fhir_types.dart'; -import 'package:faiadashu/l10n/l10n.dart'; import 'package:faiadashu/questionnaires/model/model.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/max_size_error.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/mime_types_error.dart'; import 'package:fhir/r4.dart'; -import 'package:filesize/filesize.dart'; class AttachmentAnswerModel extends AnswerModel { final num maxSize; @@ -10,14 +10,18 @@ class AttachmentAnswerModel extends AnswerModel { AttachmentAnswerModel(super.responseModel) : maxSize = responseModel.questionnaireItem.extension_ - ?.extensionOrNull('http://hl7.org/fhir/StructureDefinition/maxSize') + ?.extensionOrNull( + 'http://hl7.org/fhir/StructureDefinition/maxSize') ?.valueDecimal - ?.value ?? 0, + ?.value ?? + 0, mimeTypes = responseModel.questionnaireItem.extension_ - ?.whereExtensionIs('http://hl7.org/fhir/StructureDefinition/mimeType') + ?.whereExtensionIs( + 'http://hl7.org/fhir/StructureDefinition/mimeType') ?.map((ext) => ext.valueCode?.value ?? '') .where((mimeType) => mimeType != '') - .toList() ?? []; + .toList() ?? + []; @override RenderingString get display => (value != null) @@ -25,25 +29,26 @@ class AttachmentAnswerModel extends AnswerModel { : RenderingString.nullText; @override - String? validateInput(Attachment? inValue) { - return validateValue(inValue); + void validateInput(Attachment? inValue) { + validateValue(inValue); } @override - String? validateValue(Attachment? inputValue) { + void validateValue(Attachment? inputValue) { if (inputValue == null) return null; if (maxSize > 0) { final attachmentSize = inputValue.size?.value; if (attachmentSize == null || attachmentSize > maxSize) { - return lookupFDashLocalizations(locale).validatorMaxSize(filesize(maxSize)); + throw MaxSizeError(nodeUid, maxSize); } } if (mimeTypes.isNotEmpty) { final attachmentMimeType = inputValue.contentType?.value; - if (attachmentMimeType == null || !mimeTypes.contains(attachmentMimeType)) { - return lookupFDashLocalizations(locale).validatorMimeTypes(mimeTypes.join(', ')); + if (attachmentMimeType == null || + !mimeTypes.contains(attachmentMimeType)) { + throw MimeTypesError(nodeUid, mimeTypes); } } diff --git a/lib/questionnaires/model/item/answer/src/boolean_answer_model.dart b/lib/questionnaires/model/item/answer/src/boolean_answer_model.dart index 076a379c..f00eaf54 100644 --- a/lib/questionnaires/model/item/answer/src/boolean_answer_model.dart +++ b/lib/questionnaires/model/item/answer/src/boolean_answer_model.dart @@ -23,12 +23,12 @@ class BooleanAnswerModel extends AnswerModel { ); @override - String? validateInput(Boolean? inValue) { + void validateInput(Boolean? inValue) { return null; } @override - String? validateValue(Boolean? inputValue) { + void validateValue(Boolean? inputValue) { return null; } diff --git a/lib/questionnaires/model/item/answer/src/coding_answer_model.dart b/lib/questionnaires/model/item/answer/src/coding_answer_model.dart index b38e86e0..775ab28a 100644 --- a/lib/questionnaires/model/item/answer/src/coding_answer_model.dart +++ b/lib/questionnaires/model/item/answer/src/coding_answer_model.dart @@ -3,6 +3,9 @@ import 'package:faiadashu/fhir_types/fhir_types.dart'; import 'package:faiadashu/l10n/l10n.dart'; import 'package:faiadashu/logging/logging.dart'; import 'package:faiadashu/questionnaires/model/model.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/max_occurs_error.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/min_occurs_error.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/single_selection_or_open_string_error.dart'; import 'package:faiadashu/questionnaires/questionnaires.dart'; import 'package:fhir/r4.dart'; @@ -154,15 +157,6 @@ class CodingAnswerModel extends AnswerModel { bool get isOptionsOrString => qi.type == QuestionnaireItemType.open_choice; - RenderingString get openLabel => RenderingString.fromText( - qi.extension_ - ?.extensionOrNull( - 'http://hl7.org/fhir/uv/sdc/StructureDefinition/questionnaire-sdc-openLabel', - ) - ?.valueString ?? - lookupFDashLocalizations(locale).fillerOpenCodingOtherLabel, - ); - String _nextOptionUid() => _answerOptions.length.toString(); void _addAnswerOptionFromValueSetCoding(Coding coding) { @@ -231,6 +225,17 @@ class CodingAnswerModel extends AnswerModel { _createAnswerOptions(); } + RenderingString getOpenLabel(FDashLocalizations localizations) { + return RenderingString.fromText( + _openLabel ?? localizations.fillerOpenCodingOtherLabel, + ); + } + + String? get _openLabel => qi.extension_ + ?.extensionOrNull( + 'http://hl7.org/fhir/uv/sdc/StructureDefinition/questionnaire-sdc-openLabel') + ?.valueString; + Iterable toDisplay({bool includeMedia = true}) { final value = this.value; if (value == null) { @@ -301,14 +306,14 @@ class CodingAnswerModel extends AnswerModel { } @override - String? validateInput(OptionsOrString? inValue) { + void validateInput(OptionsOrString? inValue) { return validateValue(inValue); } @override - String? validateValue(OptionsOrString? inValue) { + void validateValue(OptionsOrString? inValue) { if (inValue == null) { - return null; + return; } final selectedOptionsCount = inValue.selectedOptions?.length ?? 0; @@ -318,21 +323,18 @@ class CodingAnswerModel extends AnswerModel { if (!(questionnaireItemModel.questionnaireItem.repeats?.value ?? false)) { if (totalCount != 1) { - return lookupFDashLocalizations(locale) - .validatorSingleSelectionOrSingleOpenString(openLabel.plainText); + throw SingleSelectionOrOpenStringError(nodeUid, _openLabel); } } if (totalCount < minOccurs) { - return lookupFDashLocalizations(locale).validatorMinOccurs(minOccurs); + throw MinOccursError(nodeUid, minOccurs); } final maxOccurs = this.maxOccurs; if (maxOccurs != null && totalCount > maxOccurs) { - return lookupFDashLocalizations(locale).validatorMaxOccurs(maxOccurs); + throw MaxOccursError(nodeUid, maxOccurs); } - - return null; } @override diff --git a/lib/questionnaires/model/item/answer/src/coding_answer_option_model.dart b/lib/questionnaires/model/item/answer/src/coding_answer_option_model.dart index e1caa725..20b41ea0 100644 --- a/lib/questionnaires/model/item/answer/src/coding_answer_option_model.dart +++ b/lib/questionnaires/model/item/answer/src/coding_answer_option_model.dart @@ -132,7 +132,7 @@ class CodingAnswerOptionModel { ?.valueString; final optionPrefix = (plainOptionPrefix != null) - ? RenderingString.fromText(plainOptionPrefix) + ? RenderingString.fromText(plainOptionPrefix, extensions: extensions) : null; RenderingString optionText; @@ -154,6 +154,7 @@ class CodingAnswerOptionModel { forDisplay = _createForDisplay(coding, locale, questionnaireItemModel); optionText = RenderingString.fromText( plainText, + extensions: extensions, ); } } else { diff --git a/lib/questionnaires/model/item/answer/src/datetime_answer_model.dart b/lib/questionnaires/model/item/answer/src/datetime_answer_model.dart index ed56b9bc..541198bd 100644 --- a/lib/questionnaires/model/item/answer/src/datetime_answer_model.dart +++ b/lib/questionnaires/model/item/answer/src/datetime_answer_model.dart @@ -1,6 +1,6 @@ import 'package:faiadashu/fhir_types/fhir_types.dart'; -import 'package:faiadashu/l10n/l10n.dart'; import 'package:faiadashu/questionnaires/model/model.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/date_time_error.dart'; import 'package:fhir/r4.dart' show Date, @@ -53,15 +53,15 @@ class DateTimeAnswerModel extends AnswerModel { } @override - String? validateInput(FhirDateTime? inValue) { + void validateInput(FhirDateTime? inValue) { return validateValue(inValue); } @override - String? validateValue(FhirDateTime? inValue) { - return inValue == null || inValue.isValid - ? null - : lookupFDashLocalizations(locale).validatorDateTime; + void validateValue(FhirDateTime? inValue) { + if (!(inValue == null || inValue.isValid)) { + throw DateTimeError(nodeUid); + } } @override @@ -81,13 +81,12 @@ class DateTimeAnswerModel extends AnswerModel { @override void populate(QuestionnaireResponseAnswer answer) { // NOTE: Model should probably be populated based on QuestionnaireItemType - value = answer.valueDateTime ?? ( - (answer.valueDate != null) - ? FhirDateTime(answer.valueDate) - : (answer.valueTime != null) - // TODO: Find a better way to convert Time values to FhirDateTime - ? FhirDateTime('1970-01-01T${answer.valueTime}') - : null - ); + value = answer.valueDateTime ?? + ((answer.valueDate != null) + ? FhirDateTime(answer.valueDate) + : (answer.valueTime != null) + // TODO: Find a better way to convert Time values to FhirDateTime + ? FhirDateTime('1970-01-01T${answer.valueTime}') + : null); } } diff --git a/lib/questionnaires/model/item/answer/src/numerical_answer_model.dart b/lib/questionnaires/model/item/answer/src/numerical_answer_model.dart index c307d8de..935a76e7 100644 --- a/lib/questionnaires/model/item/answer/src/numerical_answer_model.dart +++ b/lib/questionnaires/model/item/answer/src/numerical_answer_model.dart @@ -1,8 +1,11 @@ import 'package:faiadashu/coding/coding.dart'; +import 'package:faiadashu/extensions/function_extension.dart'; import 'package:faiadashu/fhir_types/fhir_types.dart'; -import 'package:faiadashu/l10n/l10n.dart'; import 'package:faiadashu/logging/logging.dart'; import 'package:faiadashu/questionnaires/model/model.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/max_value_error.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/min_value_error.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/nan_error.dart'; import 'package:fhir/r4.dart'; import 'package:intl/intl.dart'; @@ -161,16 +164,21 @@ class NumericalAnswerModel extends AnswerModel { } qi.extension_ - ?.whereExtensionIs('http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption') - ?.forEach((extension) { - final coding = extension.valueCoding; - if (coding == null) return; - _units[keyForUnitChoice(coding)] = coding; - }); + ?.whereExtensionIs( + 'http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption') + ?.forEach((extension) { + final coding = extension.valueCoding; + if (coding == null) return; + _units[keyForUnitChoice(coding)] = coding; + }); // Using updated usage for questionnaire-unit in R5 (http://hl7.org/fhir/extensions/StructureDefinition-questionnaire-unit.html) - final questionnaireUnit = qi.extension_?.extensionOrNull('http://hl7.org/fhir/StructureDefinition/questionnaire-unit')?.valueCoding; - if (questionnaireUnit != null && questionnaireUnit.display != null) _units[keyForUnitChoice(questionnaireUnit)] = questionnaireUnit; + final questionnaireUnit = qi.extension_ + ?.extensionOrNull( + 'http://hl7.org/fhir/StructureDefinition/questionnaire-unit') + ?.valueCoding; + if (questionnaireUnit != null && questionnaireUnit.display != null) + _units[keyForUnitChoice(questionnaireUnit)] = questionnaireUnit; } @override @@ -181,9 +189,9 @@ class NumericalAnswerModel extends AnswerModel { : RenderingString.nullText; @override - String? validateInput(String? inputValue) { + void validateInput(String? inputValue) { if (inputValue == null || inputValue.isEmpty) { - return null; + return; } num number = double.nan; try { @@ -191,17 +199,17 @@ class NumericalAnswerModel extends AnswerModel { } catch (_) { // Ignore FormatException, number remains nan. } - if (number == double.nan) { - return lookupFDashLocalizations(locale).validatorNan; + if (number.isNaN) { + throw NanError(nodeUid); } final quantity = _valueFromNumber(number); - return validateValue(quantity); + validateValue(quantity); } @override - String? validateValue(Quantity? inputValue) { + void validateValue(Quantity? inputValue) { if (inputValue == null) { return null; } @@ -213,12 +221,10 @@ class NumericalAnswerModel extends AnswerModel { } if (number > _maxValue) { - return lookupFDashLocalizations(locale) - .validatorMaxValue(Decimal(_maxValue).format(locale)); + throw MaxValueError(nodeUid, Decimal(_maxValue).format(locale)); } if (number < _minValue) { - return lookupFDashLocalizations(locale) - .validatorMinValue(Decimal(_minValue).format(locale)); + throw MinValueError(nodeUid, Decimal(_minValue).format(locale)); } return null; @@ -259,7 +265,8 @@ class NumericalAnswerModel extends AnswerModel { /// * Updates the numerical value based on text input /// * Keeps the unit Quantity? copyWithTextInput(String textInput) { - final valid = validateInput(textInput) == null; + final valid = (() => validateInput(textInput)).callSafely() ?? false; + final dataAbsentReasonExtension = !valid ? [ FhirExtension( diff --git a/lib/questionnaires/model/item/answer/src/string_answer_model.dart b/lib/questionnaires/model/item/answer/src/string_answer_model.dart index d8b11ab0..eb7077a3 100644 --- a/lib/questionnaires/model/item/answer/src/string_answer_model.dart +++ b/lib/questionnaires/model/item/answer/src/string_answer_model.dart @@ -1,7 +1,10 @@ import 'package:faiadashu/coding/coding.dart'; import 'package:faiadashu/fhir_types/fhir_types.dart'; -import 'package:faiadashu/l10n/l10n.dart'; import 'package:faiadashu/questionnaires/model/model.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/entry_format_error.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/min_length_error.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/regex_error.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/url_error.dart'; import 'package:fhir/r4.dart'; enum StringAnswerKeyboard { plain, email, phone, number, multiline, url } @@ -61,73 +64,73 @@ class StringAnswerModel extends AnswerModel { : RenderingString.nullText; @override - String? validateInput(String? inValue) { + void validateInput(String? inValue) { final checkValue = inValue?.trim(); return validateValue(checkValue); } @override - String? validateValue(String? inputValue) { + void validateValue(String? inputValue) { if (inputValue == null || inputValue.isEmpty) { - return null; + return; } if (inputValue.length < minLength) { - return lookupFDashLocalizations(locale).validatorMinLength(minLength); + throw MinLengthError(nodeUid, minLength); } if (maxLength != null && inputValue.length > maxLength!) { - return lookupFDashLocalizations(locale).validatorMaxLength(maxLength!); + throw MinLengthError(nodeUid, maxLength!); } if (qi.type == QuestionnaireItemType.url) { if (!_urlRegExp.hasMatch(inputValue)) { - return lookupFDashLocalizations(locale).validatorUrl; + throw UrlError(nodeUid); } } if (regExp != null) { if (!regExp!.hasMatch(inputValue)) { - return (entryFormat != null) - ? lookupFDashLocalizations(locale) - .validatorEntryFormat(entryFormat!) - : lookupFDashLocalizations(locale).validatorRegExp; + throw (entryFormat != null) + ? EntryFormatError(nodeUid, entryFormat!) + : RegexError(nodeUid); } } - - return null; } @override QuestionnaireResponseAnswer? createFhirAnswer( List? items, ) { - final value = this.value?.trim(); - - final valid = validateInput(value) == null; - final dataAbsentReasonExtension = !valid - ? [ - FhirExtension( - url: dataAbsentReasonExtensionUrl, - valueCode: dataAbsentReasonAsTextCode, - ), - ] - : null; - - return (value != null && value.isNotEmpty) - ? (qi.type != QuestionnaireItemType.url) - ? QuestionnaireResponseAnswer( - valueString: value, - extension_: dataAbsentReasonExtension, - item: items, - ) - : QuestionnaireResponseAnswer( - valueUri: FhirUri(value), - extension_: dataAbsentReasonExtension, - item: items, - ) - : null; + try { + final value = this.value?.trim(); + + validateInput(value); + + final dataAbsentReasonExtension = [ + FhirExtension( + url: dataAbsentReasonExtensionUrl, + valueCode: dataAbsentReasonAsTextCode, + ), + ]; + + return (value != null && value.isNotEmpty) + ? (qi.type != QuestionnaireItemType.url) + ? QuestionnaireResponseAnswer( + valueString: value, + extension_: dataAbsentReasonExtension, + item: items, + ) + : QuestionnaireResponseAnswer( + valueUri: FhirUri(value), + extension_: dataAbsentReasonExtension, + item: items, + ) + : null; + } on Exception { + return null; + } } @override diff --git a/lib/questionnaires/model/item/answer/src/unsupported_answer_model.dart b/lib/questionnaires/model/item/answer/src/unsupported_answer_model.dart index f16712ef..f37c9e60 100644 --- a/lib/questionnaires/model/item/answer/src/unsupported_answer_model.dart +++ b/lib/questionnaires/model/item/answer/src/unsupported_answer_model.dart @@ -16,12 +16,12 @@ class UnsupportedAnswerModel extends AnswerModel { RenderingString get display => RenderingString.nullText; @override - String? validateInput(Object? inValue) { + void validateInput(Object? inValue) { return null; } @override - String? validateValue(Object? inputValue) { + void validateValue(Object? inputValue) { return null; } diff --git a/lib/questionnaires/model/item/src/question_item_model.dart b/lib/questionnaires/model/item/src/question_item_model.dart index 76ed1df1..0f66ed02 100644 --- a/lib/questionnaires/model/item/src/question_item_model.dart +++ b/lib/questionnaires/model/item/src/question_item_model.dart @@ -1,10 +1,8 @@ import 'dart:developer'; import 'package:collection/collection.dart'; -import 'package:faiadashu/coding/coding.dart'; -import 'package:faiadashu/fhir_types/fhir_types.dart'; -import 'package:faiadashu/logging/logging.dart'; -import 'package:faiadashu/questionnaires/questionnaires.dart'; +import 'package:faiadashu/faiadashu.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; import 'package:fhir/primitive_types/primitive_types.dart'; import 'package:fhir/r4/r4.dart'; import 'package:fhir_path/fhir_path.dart'; @@ -172,36 +170,27 @@ class QuestionItemModel extends ResponseItemModel { } @override - Map? validate({ + List validate({ bool updateErrorText = true, bool notifyListeners = false, }) { // Non-existent answer models can be invalid, e.g. if minOccurs is not met. _ensureAnswerModel(); - final responseErrorTexts = super.validate( - updateErrorText: updateErrorText, - notifyListeners: notifyListeners, - ) ?? - {}; + List errors = super.validate( + updateErrorText: updateErrorText, + notifyListeners: notifyListeners, + ); - final answersErrorTexts = {}; for (final am in answerModels) { - final answerValidationText = am.validate( + final amErrors = am.validate( updateErrorText: updateErrorText, notifyListeners: notifyListeners, ); - - if (answerValidationText != null) { - answersErrorTexts[am.nodeUid] = answerValidationText; - } + errors.addAll(amErrors); } - final combinedErrorTexts = responseErrorTexts..addAll(answersErrorTexts); - - return responseErrorTexts.isEmpty && answersErrorTexts.isEmpty - ? null - : combinedErrorTexts; + return errors; } @override @@ -430,8 +419,9 @@ class QuestionItemModel extends ResponseItemModel { // Write the value back to the answer model firstAnswerModel.populateFromExpression(evaluationResult); } catch (ex) { - errorText = - (ex is FhirPathEvaluationException) ? ex.message : ex.toString(); + // errorText = + // (ex is FhirPathEvaluationException) ? ex.message : ex.toString(); + if (ex is FhirPathEvaluationException) {} _qimLogger.warn('Calculation problem: $_calculatedExpression', error: ex); notifyListeners(); // This could be added to a setter for errorText, but might have side-effects. } diff --git a/lib/questionnaires/model/item/src/response_item_model.dart b/lib/questionnaires/model/item/src/response_item_model.dart index b48ead2e..0a7c434d 100644 --- a/lib/questionnaires/model/item/src/response_item_model.dart +++ b/lib/questionnaires/model/item/src/response_item_model.dart @@ -1,6 +1,8 @@ -import 'package:faiadashu/l10n/l10n.dart'; -import 'package:faiadashu/logging/logging.dart'; -import 'package:faiadashu/questionnaires/model/model.dart'; +import 'package:faiadashu/faiadashu.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/constraint_validation_error.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/custom_validation_error.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/required_item_error.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; import 'package:fhir/r4.dart'; /// Model a response item @@ -48,49 +50,45 @@ abstract class ResponseItemModel extends FillerItemModel { /// Returns a description of the current error situation with this item. /// /// Localized text if an error exists. Or null if no error exists. - String? errorText; + String? getErrorText(FDashLocalizations localizations) { + return _exception?.getMessage(localizations); + } + + ValidationError? _exception; - Map? validate({ + List validate({ bool updateErrorText = true, bool notifyListeners = false, }) { - String? newErrorText; - if (questionnaireItemModel.isRequired && isUnanswered) { - newErrorText = lookupFDashLocalizations(questionnaireResponseModel.locale) - .validatorRequiredItem; + return [RequiredItemError(nodeUid)]; } - - final constraintError = validateConstraint(); - newErrorText ??= constraintError; - - if (errorText != newErrorText) { - if (updateErrorText) { - errorText = newErrorText; - } - if (notifyListeners) { - this.notifyListeners(); + try { + validateConstraint(); + _exception = null; + } on ValidationError catch (exception) { + _exception ??= exception; + + if (_exception != exception) { + if (updateErrorText) { + _exception = exception; + } + if (notifyListeners) { + this.notifyListeners(); + } } + return [_exception!]; } - - if (newErrorText == null) { - return null; - } else { - final resultMap = {}; - resultMap[nodeUid] = newErrorText; - - return resultMap; - } + return []; } /// Returns whether the item is satisfying the `questionnaire-constraint`. /// - /// Returns null if satisfied, or a human-readable text if not satisfied. - /// Returns null if no constraint is specified. - String? validateConstraint() { + /// Throws [CustomValidationError] with human-readable text if not satisfied. + void validateConstraint() { final constraintExpression = _constraintExpression; if (constraintExpression == null) { - return null; + return; } final isSatisfied = constraintExpression.fetchBoolValue( @@ -99,6 +97,11 @@ abstract class ResponseItemModel extends FillerItemModel { location: nodeUid, ); - return isSatisfied ? null : questionnaireItemModel.constraintHuman; + if (!isSatisfied) { + throw ConstraintValidationError( + nodeUid, + questionnaireItemModel.constraintHuman, + ); + } } } diff --git a/lib/questionnaires/model/src/questionnaire_response_model.dart b/lib/questionnaires/model/src/questionnaire_response_model.dart index c979c795..f53dd19b 100644 --- a/lib/questionnaires/model/src/questionnaire_response_model.dart +++ b/lib/questionnaires/model/src/questionnaire_response_model.dart @@ -1,11 +1,11 @@ -import 'dart:ui'; - import 'package:collection/collection.dart'; +import 'package:faiadashu/l10n/l10n.dart'; import 'package:faiadashu/logging/logging.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; import 'package:faiadashu/questionnaires/questionnaires.dart'; import 'package:faiadashu/resource_provider/resource_provider.dart'; import 'package:fhir/r4.dart'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; /// High-level model of a response to a questionnaire. class QuestionnaireResponseModel { @@ -160,6 +160,7 @@ class QuestionnaireResponseModel { List? aggregators, required FhirResourceProvider fhirResourceProvider, required LaunchContext launchContext, + required FDashLocalizations localizations, QuestionnaireModelDefaults questionnaireModelDefaults = const QuestionnaireModelDefaults(), }) async { @@ -178,9 +179,9 @@ class QuestionnaireResponseModel { questionnaireModel: questionnaireModel, aggregators: aggregators ?? [ - TotalScoreAggregator(), - NarrativeAggregator(), - QuestionnaireResponseAggregator(), + TotalScoreAggregator(localizations: localizations), + NarrativeAggregator(localizations: localizations), + QuestionnaireResponseAggregator(localizations: localizations), ], launchContext: launchContext, ); @@ -824,8 +825,10 @@ class QuestionnaireResponseModel { /// /// Will return the parent [QuestionItemModel] if uid corresponds to an answer. FillerItemModel? fillerItemModelByUid(String uid) { - final fillerItem = - orderedFillerItemModels().firstWhereOrNull((fim) => fim.nodeUid == uid); + final fillerItem = orderedFillerItemModels().firstWhereOrNull((fim) { + print("UID:" + fim.nodeUid); + return fim.nodeUid == uid; + }); if (fillerItem != null) { return fillerItem; } @@ -848,20 +851,20 @@ class QuestionnaireResponseModel { /// Returns null, if everything is complete. /// Returns a map (UID -> error text) with incomplete entries, if items are incomplete. Map? validate({ + required FDashLocalizations localizations, bool updateErrorText = true, bool notifyListeners = false, }) { final invalidMap = {}; for (final itemModel in orderedResponseItemModels()) { - final errorTexts = itemModel.validate( - updateErrorText: updateErrorText, - notifyListeners: notifyListeners, - ); - if (errorTexts != null) { + try { + itemModel.validate( + updateErrorText: updateErrorText, + notifyListeners: notifyListeners, + ); + } on ValidationError catch (exception) { _logger.debug('$itemModel is invalid.'); - - invalidMap.addAll(errorTexts); } } diff --git a/lib/questionnaires/model/src/rendering_string.dart b/lib/questionnaires/model/src/rendering_string.dart index 7a05287b..20607959 100644 --- a/lib/questionnaires/model/src/rendering_string.dart +++ b/lib/questionnaires/model/src/rendering_string.dart @@ -33,6 +33,8 @@ class RenderingString with Diagnosticable { /// The unaltered rendering-markdown extension final Markdown? renderingMarkdown; + final List? extensions; + /// Construct an [RenderingString] from the provided attributes. /// /// No alterations of the attributes will take place, in particular, @@ -44,6 +46,7 @@ class RenderingString with Diagnosticable { this.renderingStyle, this.renderingXhtml, this.renderingMarkdown, + this.extensions, }); /// Construct an [RenderingString] from plainText and optional extensions. @@ -107,6 +110,7 @@ class RenderingString with Diagnosticable { renderingStyle: renderingStyle, renderingXhtml: renderingXhtml, renderingMarkdown: renderingMarkdown, + extensions: extensions, ); } diff --git a/lib/questionnaires/view/item/answer/src/attachment_answer_filler.dart b/lib/questionnaires/view/item/answer/src/attachment_answer_filler.dart index df9c467a..2e4f904b 100644 --- a/lib/questionnaires/view/item/answer/src/attachment_answer_filler.dart +++ b/lib/questionnaires/view/item/answer/src/attachment_answer_filler.dart @@ -1,4 +1,5 @@ import 'package:faiadashu/fhir_types/fhir_types.dart'; +import 'package:faiadashu/l10n/l10n.dart'; import 'package:faiadashu/questionnaires/questionnaires.dart'; import 'package:fhir/r4.dart'; import 'package:flutter/material.dart'; @@ -29,7 +30,8 @@ class _AttachmentAnswerState extends QuestionnaireAnswerFillerState { +class _AttachmentInputControl + extends AnswerInputControl { const _AttachmentInputControl( AttachmentAnswerModel answerModel, { FocusNode? focusNode, @@ -47,7 +49,7 @@ class _AttachmentInputControl extends AnswerInputControl allowedMimeTypes: answerModel.mimeTypes, onChanged: (attachment) => answerModel.value = attachment, decoration: InputDecoration( - errorText: answerModel.displayErrorText, + errorText: answerModel.displayErrorText(FDashLocalizations.of(context)), ), ); } diff --git a/lib/questionnaires/view/item/answer/src/boolean_answer_filler.dart b/lib/questionnaires/view/item/answer/src/boolean_answer_filler.dart index f924b27a..6426a498 100644 --- a/lib/questionnaires/view/item/answer/src/boolean_answer_filler.dart +++ b/lib/questionnaires/view/item/answer/src/boolean_answer_filler.dart @@ -1,3 +1,4 @@ +import 'package:faiadashu/l10n/l10n.dart'; import 'package:faiadashu/questionnaires/questionnaires.dart'; import 'package:fhir/r4.dart'; import 'package:flutter/material.dart'; @@ -47,9 +48,11 @@ class _BooleanInputControl extends AnswerInputControl { value: (answerModel.isTriState) ? answerModel.value?.value : (answerModel.value?.value != null), - activeColor: (answerModel.displayErrorText != null) - ? Theme.of(context).errorColor - : null, + activeColor: + (answerModel.displayErrorText(FDashLocalizations.of(context)) != + null) + ? Theme.of(context).errorColor + : null, tristate: answerModel.isTriState, onChanged: (answerModel.isControlEnabled) ? (newValue) { @@ -62,9 +65,10 @@ class _BooleanInputControl extends AnswerInputControl { } : null, ), - if (answerModel.displayErrorText != null) + if (answerModel.displayErrorText(FDashLocalizations.of(context)) != + null) Text( - answerModel.displayErrorText!, + answerModel.displayErrorText(FDashLocalizations.of(context))!, style: Theme.of(context) .textTheme .caption! diff --git a/lib/questionnaires/view/item/answer/src/coding_answer_filler.dart b/lib/questionnaires/view/item/answer/src/coding_answer_filler.dart index 2507cecd..eb55ff52 100644 --- a/lib/questionnaires/view/item/answer/src/coding_answer_filler.dart +++ b/lib/questionnaires/view/item/answer/src/coding_answer_filler.dart @@ -43,13 +43,20 @@ class _CodingInputControl extends AnswerInputControl { @override Widget build(BuildContext context) { - final errorText = answerModel.displayErrorText; + final errorText = + answerModel.displayErrorText(FDashLocalizations.of(context)); - return QuestionnaireTheme.of(context).codingControlLayoutBuilder( - context, - _buildCodingControl(context), - openStringInputControlWidget: answerModel.isOptionsOrString ? _OpenStringInputControl(answerModel) : null, - errorText: errorText, + return Column( + children: [ + QuestionnaireTheme.of(context).codingControlLayoutBuilder( + context, + _buildCodingControl(context), + openStringInputControlWidget: answerModel.isOptionsOrString + ? _OpenStringInputControl(answerModel) + : null, + errorText: errorText, + ) + ], ); } @@ -314,7 +321,7 @@ class _CodingDropdown extends AnswerInputControl { // Empty error texts triggers red border, but showing text would result in a duplicate. errorStyle: const TextStyle(height: 0, color: Color.fromARGB(0, 0, 0, 0)), - errorText: answerModel.displayErrorText, + errorText: answerModel.displayErrorText(FDashLocalizations.of(context)), ), ); } @@ -472,7 +479,9 @@ class _CodingChoiceDecorator extends StatelessWidget { child: AnimatedBuilder( animation: Focus.of(context), builder: (context, child) { - final hasError = answerModel.displayErrorText != null; + final hasError = + answerModel.displayErrorText(FDashLocalizations.of(context)) != + null; final decoTheme = Theme.of(context).inputDecorationTheme; // TODO: Return something borderless when filled = true @@ -591,7 +600,7 @@ class _OpenStringInputControlState extends State<_OpenStringInputControl> { children: [ Xhtml.fromRenderingString( context, - answerModel.openLabel, + answerModel.getOpenLabel(FDashLocalizations.of(context)), defaultTextStyle: Theme.of(context) .textTheme .bodyText2 @@ -615,7 +624,8 @@ class _OpenStringInputControlState extends State<_OpenStringInputControl> { errorStyle: const TextStyle(height: 0, color: Color.fromARGB(0, 0, 0, 0)), - errorText: answerModel.displayErrorText, + errorText: + answerModel.displayErrorText(FDashLocalizations.of(context)), ), ), ), @@ -650,7 +660,7 @@ class _FDashAutocompleteField extends StatelessWidget { onFieldSubmitted(); }, decoration: InputDecoration( - errorText: answerModel.displayErrorText, + errorText: answerModel.displayErrorText(FDashLocalizations.of(context)), hintText: FDashLocalizations.of(context).autoCompleteSearchTermInput, ), ); diff --git a/lib/questionnaires/view/item/answer/src/datetime_answer_filler.dart b/lib/questionnaires/view/item/answer/src/datetime_answer_filler.dart index 5c86ceec..910fcf61 100644 --- a/lib/questionnaires/view/item/answer/src/datetime_answer_filler.dart +++ b/lib/questionnaires/view/item/answer/src/datetime_answer_filler.dart @@ -1,4 +1,5 @@ import 'package:faiadashu/fhir_types/fhir_types.dart'; +import 'package:faiadashu/l10n/l10n.dart'; import 'package:faiadashu/questionnaires/questionnaires.dart'; import 'package:fhir/r4.dart' show Date, FhirDateTime, QuestionnaireItemType, Time; @@ -65,7 +66,7 @@ class _DateTimeInputControl extends AnswerInputControl { datePickerEntryMode: QuestionnaireTheme.of(context).datePickerEntryMode, timePickerEntryMode: QuestionnaireTheme.of(context).timePickerEntryMode, decoration: InputDecoration( - errorText: answerModel.displayErrorText, + errorText: answerModel.displayErrorText(FDashLocalizations.of(context)), errorStyle: (itemModel .isCalculated) // Force display of error text on calculated item ? TextStyle( diff --git a/lib/questionnaires/view/item/answer/src/numerical_answer_filler.dart b/lib/questionnaires/view/item/answer/src/numerical_answer_filler.dart index 09f149c4..6ae3adff 100644 --- a/lib/questionnaires/view/item/answer/src/numerical_answer_filler.dart +++ b/lib/questionnaires/view/item/answer/src/numerical_answer_filler.dart @@ -1,4 +1,6 @@ import 'package:faiadashu/fhir_types/fhir_types.dart'; +import 'package:faiadashu/l10n/l10n.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/custom_validation_error.dart'; import 'package:faiadashu/questionnaires/questionnaires.dart'; import 'package:fhir/r4.dart'; import 'package:flutter/material.dart'; @@ -195,7 +197,8 @@ class _NumberFieldInputControl textAlignVertical: TextAlignVertical.center, textAlign: TextAlign.end, decoration: InputDecoration( - errorText: answerModel.displayErrorText, + errorText: + answerModel.displayErrorText(FDashLocalizations.of(context)), errorStyle: (itemModel .isCalculated) // Force display of error text on calculated item ? TextStyle( @@ -206,7 +209,9 @@ class _NumberFieldInputControl prefixIcon: itemModel.isCalculated ? Icon( Icons.calculate, - color: (answerModel.displayErrorText != null) + color: (answerModel.displayErrorText( + FDashLocalizations.of(context)) != + null) ? Theme.of(context).errorColor : null, ) @@ -222,11 +227,16 @@ class _NumberFieldInputControl signed: answerModel.minValue < 0, decimal: answerModel.maxDecimal > 0, ), - validator: (itemModel.isCalculated) - ? null - : (inputValue) { - return answerModel.validateInput(inputValue); - }, + validator: (inputValue) { + try { + if (itemModel.isCalculated) { + answerModel.validateInput(inputValue); + } + } on CustomValidationError catch (exception) { + return exception.getMessage(FDashLocalizations.of(context)); + } + return null; + }, autovalidateMode: (itemModel.isCalculated) ? AutovalidateMode.disabled : AutovalidateMode.always, @@ -237,11 +247,11 @@ class _NumberFieldInputControl // - hasSingleUnitChoice = true (no dropdown shown), or // - user has not interacted with the unit dropdown. value = answerModel - .copyWithUnit(answerModel.unitChoices.first.code?.value) - ?.copyWith( - value: value?.value, - extension_: value?.extension_, - ); + .copyWithUnit(answerModel.unitChoices.first.code?.value) + ?.copyWith( + value: value?.value, + extension_: value?.extension_, + ); } answerModel.value = value; }, @@ -276,7 +286,8 @@ class _UnitDropDown extends AnswerInputControl { width: unitWidth, child: DropdownButtonHideUnderline( child: DropdownButton( - value: answerModel.keyOfUnit ?? answerModel.unitChoices.first.code?.value, + value: answerModel.keyOfUnit ?? + answerModel.unitChoices.first.code?.value, hint: const NullDashText(), onChanged: (answerModel.isControlEnabled) ? (String? newValue) { diff --git a/lib/questionnaires/view/item/answer/src/string_answer_filler.dart b/lib/questionnaires/view/item/answer/src/string_answer_filler.dart index f37ea7e0..e34cb73e 100644 --- a/lib/questionnaires/view/item/answer/src/string_answer_filler.dart +++ b/lib/questionnaires/view/item/answer/src/string_answer_filler.dart @@ -1,3 +1,5 @@ +import 'package:faiadashu/l10n/l10n.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/custom_validation_error.dart'; import 'package:faiadashu/questionnaires/questionnaires.dart'; import 'package:fhir/r4.dart'; import 'package:flutter/material.dart'; @@ -94,7 +96,7 @@ class _StringAnswerInputControl extends AnswerInputControl { ? QuestionnaireTheme.of(context).maxLinesForTextItem : 1, decoration: InputDecoration( - errorText: answerModel.displayErrorText, + errorText: answerModel.displayErrorText(FDashLocalizations.of(context)), errorStyle: (itemModel .isCalculated) // Force display of error text on calculated item ? TextStyle( @@ -105,13 +107,22 @@ class _StringAnswerInputControl extends AnswerInputControl { prefixIcon: itemModel.isCalculated ? Icon( Icons.calculate, - color: (answerModel.displayErrorText != null) + color: (answerModel + .displayErrorText(FDashLocalizations.of(context)) != + null) ? Theme.of(context).errorColor : null, ) : null, ), - validator: (inputValue) => answerModel.validateInput(inputValue), + validator: (inputValue) { + try { + answerModel.validateInput(inputValue); + } on CustomValidationError catch (exception) { + return exception.getMessage(FDashLocalizations.of(context)); + } + return null; + }, autovalidateMode: AutovalidateMode.always, onChanged: (content) { answerModel.value = content; diff --git a/lib/questionnaires/view/item/src/group_item.dart b/lib/questionnaires/view/item/src/group_item.dart index 1f84da43..8274cc26 100644 --- a/lib/questionnaires/view/item/src/group_item.dart +++ b/lib/questionnaires/view/item/src/group_item.dart @@ -1,3 +1,4 @@ +import 'package:faiadashu/l10n/l10n.dart'; import 'package:faiadashu/logging/logging.dart'; import 'package:faiadashu/questionnaires/questionnaires.dart'; import 'package:flutter/material.dart'; @@ -30,7 +31,9 @@ class _GroupItemState extends ResponseItemFillerState { return AnimatedBuilder( animation: widget.responseItemModel, builder: (context, _) { - final errorText = widget.responseItemModel.errorText; + final errorText = widget.responseItemModel.getErrorText( + FDashLocalizations.of(context), + ); return widget.responseItemModel.displayVisibility != DisplayVisibility.hidden diff --git a/lib/questionnaires/view/item/src/question_response_item_filler.dart b/lib/questionnaires/view/item/src/question_response_item_filler.dart index ae340549..cc6eaac0 100644 --- a/lib/questionnaires/view/item/src/question_response_item_filler.dart +++ b/lib/questionnaires/view/item/src/question_response_item_filler.dart @@ -79,9 +79,8 @@ class QuestionResponseItemFillerState Widget? _questionSkipperWidget() { if (questionnaireTheme.canSkipQuestions && - !widget.questionnaireItemModel.isReadOnly && - !widget.questionnaireItemModel.isRequired - ) { + !widget.questionnaireItemModel.isReadOnly && + !widget.questionnaireItemModel.isRequired) { return Row( children: [ Text( @@ -121,7 +120,8 @@ class QuestionResponseItemFillerState debugDumpFocusTree(); }, */ focusNode: focusNode, - child: QuestionnaireTheme.of(context).questionResponseItemLayoutBuilder( + child: QuestionnaireTheme.of(context) + .questionResponseItemLayoutBuilder( context, widget.responseItemModel as QuestionItemModel, _answerFillerWidget(), diff --git a/lib/questionnaires/view/item/src/questionnaire_item_filler.dart b/lib/questionnaires/view/item/src/questionnaire_item_filler.dart index 9b6a6a49..c595e392 100644 --- a/lib/questionnaires/view/item/src/questionnaire_item_filler.dart +++ b/lib/questionnaires/view/item/src/questionnaire_item_filler.dart @@ -17,7 +17,11 @@ abstract class QuestionnaireItemFiller extends StatefulWidget { abstract class QuestionnaireItemFillerState extends State { - late final Widget? _titleWidget; + late final Widget? _titleWidget = QuestionnaireItemFillerTitle.fromFillerItem( + context: context, + fillerItem: widget.fillerItemModel, + questionnaireTheme: questionnaireTheme, + ); Widget? get titleWidget => _titleWidget; QuestionnaireFillerData? _questionnaireFiller; @@ -32,11 +36,6 @@ abstract class QuestionnaireItemFillerState void initState() { super.initState(); _focusNode = FocusNode(debugLabel: responseUid, skipTraversal: true); - - _titleWidget = QuestionnaireItemFillerTitle.fromFillerItem( - fillerItem: widget.fillerItemModel, - questionnaireTheme: questionnaireTheme, - ); } @override diff --git a/lib/questionnaires/view/item/src/questionnaire_item_filler_title.dart b/lib/questionnaires/view/item/src/questionnaire_item_filler_title.dart index f0a25ce8..cb8d2015 100644 --- a/lib/questionnaires/view/item/src/questionnaire_item_filler_title.dart +++ b/lib/questionnaires/view/item/src/questionnaire_item_filler_title.dart @@ -1,3 +1,4 @@ +import 'package:faiadashu/extensions/string_extension.dart'; import 'package:faiadashu/fhir_types/fhir_types.dart'; import 'package:faiadashu/logging/logging.dart'; import 'package:faiadashu/questionnaires/questionnaires.dart'; @@ -19,6 +20,7 @@ class QuestionnaireItemFillerTitle extends StatelessWidget { }) : super(key: key); static Widget? fromFillerItem({ + required BuildContext context, required FillerItemModel fillerItem, required QuestionnaireThemeData questionnaireTheme, Key? key, @@ -48,10 +50,14 @@ class QuestionnaireItemFillerTitle extends StatelessWidget { : '

'; final prefixText = fillerItem.prefix; + final title = text.xhtmlText.translate( + questionnaireItemModel.text?.extensions ?? [], + Localizations.localeOf(context), + ); final htmlTitleText = (prefixText != null) - ? '$openStyleTag${prefixText.xhtmlText} ${text.xhtmlText}$requiredTag$closeStyleTag' - : '$openStyleTag${text.xhtmlText}$requiredTag$closeStyleTag'; + ? '$openStyleTag${prefixText} ${title}$requiredTag$closeStyleTag' + : '$openStyleTag${title}$requiredTag$closeStyleTag'; return QuestionnaireItemFillerTitle._( htmlTitleText: htmlTitleText, diff --git a/lib/questionnaires/view/src/questionnaire_complete_button.dart b/lib/questionnaires/view/src/questionnaire_complete_button.dart index 3c3f0366..eaf36c2d 100644 --- a/lib/questionnaires/view/src/questionnaire_complete_button.dart +++ b/lib/questionnaires/view/src/questionnaire_complete_button.dart @@ -31,7 +31,10 @@ class _QuestionnaireCompleteButtonState final currentResponseStatus = qrm.responseStatus; if (currentResponseStatus != QuestionnaireResponseStatus.completed) { - final incompleteItems = qrm.validate(notifyListeners: true); + final incompleteItems = qrm.validate( + localizations: FDashLocalizations.of(context), + notifyListeners: true, + ); qrm.invalidityNotifier.value = incompleteItems; if (incompleteItems != null) { diff --git a/lib/questionnaires/view/src/questionnaire_filler.dart b/lib/questionnaires/view/src/questionnaire_filler.dart index babc906d..3a0faca4 100644 --- a/lib/questionnaires/view/src/questionnaire_filler.dart +++ b/lib/questionnaires/view/src/questionnaire_filler.dart @@ -1,3 +1,4 @@ +import 'package:faiadashu/l10n/l10n.dart'; import 'package:faiadashu/logging/logging.dart'; import 'package:faiadashu/questionnaires/questionnaires.dart'; import 'package:faiadashu/resource_provider/resource_provider.dart'; @@ -13,7 +14,6 @@ import 'package:flutter/material.dart'; /// see: [QuestionnaireScrollerPage] /// see: [QuestionnaireStepperPage] class QuestionnaireResponseFiller extends StatefulWidget { - final Locale locale; final WidgetBuilder builder; final List>? aggregators; final void Function(BuildContext context, Uri url)? onLinkTap; @@ -24,19 +24,19 @@ class QuestionnaireResponseFiller extends StatefulWidget { final FhirResourceProvider fhirResourceProvider; final LaunchContext launchContext; - Future - _createQuestionnaireResponseModel() async => - QuestionnaireResponseModel.fromFhirResourceBundle( - locale: locale, - aggregators: aggregators, - fhirResourceProvider: fhirResourceProvider, - launchContext: launchContext, - questionnaireModelDefaults: questionnaireModelDefaults, - ); + Future _createQuestionnaireResponseModel({ + required BuildContext context, + }) async => + QuestionnaireResponseModel.fromFhirResourceBundle( + locale: Localizations.localeOf(context), + aggregators: aggregators, + fhirResourceProvider: fhirResourceProvider, + launchContext: launchContext, + questionnaireModelDefaults: questionnaireModelDefaults, + localizations: FDashLocalizations.of(context)); const QuestionnaireResponseFiller({ Key? key, - required this.locale, required this.builder, required this.fhirResourceProvider, required this.launchContext, @@ -65,18 +65,11 @@ class _QuestionnaireResponseFillerState extends State { static final _logger = Logger(_QuestionnaireResponseFillerState); - late final Future builderFuture; QuestionnaireResponseModel? _questionnaireResponseModel; VoidCallback? _handleQuestionnaireResponseModelChangeListenerFunction; // ignore: use_late_for_private_fields_and_variables QuestionnaireFillerData? _questionnaireFillerData; - @override - void initState() { - super.initState(); - builderFuture = widget._createQuestionnaireResponseModel(); - } - @override void didChangeDependencies() { super.didChangeDependencies(); @@ -110,7 +103,6 @@ class _QuestionnaireResponseFillerState () { _questionnaireFillerData = QuestionnaireFillerData._( _questionnaireResponseModel!, - locale: widget.locale, builder: widget.builder, onLinkTap: widget.onLinkTap, onDataAvailable: widget.onDataAvailable, @@ -134,7 +126,7 @@ class _QuestionnaireResponseFillerState _logger.trace('Enter build()'); return FutureBuilder( - future: builderFuture, + future: widget._createQuestionnaireResponseModel(context: context), builder: (context, snapshot) { switch (snapshot.connectionState) { case ConnectionState.active: @@ -168,7 +160,6 @@ class _QuestionnaireResponseFillerState _questionnaireFillerData = QuestionnaireFillerData._( _questionnaireResponseModel!, - locale: widget.locale, builder: widget.builder, onLinkTap: widget.onLinkTap, onDataAvailable: widget.onDataAvailable, @@ -191,7 +182,6 @@ class _QuestionnaireResponseFillerState class QuestionnaireFillerData extends InheritedWidget { static final _logger = Logger(QuestionnaireFillerData); - final Locale locale; final QuestionnaireResponseModel questionnaireResponseModel; // TODO: Should this copy exist, or just refer to the qrm as the source of truth? final List fillerItemModels; @@ -206,7 +196,6 @@ class QuestionnaireFillerData extends InheritedWidget { QuestionnaireFillerData._( this.questionnaireResponseModel, { Key? key, - required this.locale, this.onDataAvailable, this.onLinkTap, required this.questionnaireTheme, @@ -258,14 +247,16 @@ class QuestionnaireFillerData extends InheritedWidget { /// The [QuestionnaireItemFiller]s are ordered based on 'pre-order'. /// /// see: https://en.wikipedia.org/wiki/Tree_traversal#Pre-order,_NLR - QuestionnaireItemFiller itemFillerAt(int index) { + QuestionnaireItemFiller itemFillerAt(BuildContext context, int index) { _logger.trace('itemFillerAt $index'); + final item = _itemFillers[index]; - if (_itemFillers[index] == null) { + if (item == null) { _logger.debug('itemFillerAt $index will be created.'); _itemFillers[index] = questionnaireTheme.createQuestionnaireItemFiller( this, fillerItemModels.elementAt(index), + Localizations.localeOf(context), key: ValueKey( 'item-filler-${fillerItemModels.elementAt(index).nodeUid}', ), @@ -284,19 +275,22 @@ class QuestionnaireFillerData extends InheritedWidget { /// that is currently visible. int indexOfVisibleItemAt(int visibleIndex, {bool rootsOnly = false}) { final visibleItems = fillerItemModels - .where((item) => item.displayVisibility != DisplayVisibility.hidden) - .where((item) => !rootsOnly || item.parentNode == null) - .toList(growable: false); + .where((item) => item.displayVisibility != DisplayVisibility.hidden) + .where((item) => !rootsOnly || item.parentNode == null) + .toList(growable: false); - return visibleItems.length > visibleIndex ? fillerItemModels.indexOf(visibleItems[visibleIndex]) : -1; + return visibleItems.length > visibleIndex + ? fillerItemModels.indexOf(visibleItems[visibleIndex]) + : -1; } /// Returns the [QuestionnaireItemFiller] of the [visibleIndex]-th item that is currently /// visible. - QuestionnaireItemFiller? visibleItemFillerAt(int visibleIndex) { + QuestionnaireItemFiller? visibleItemFillerAt( + BuildContext context, int visibleIndex) { final index = indexOfVisibleItemAt(visibleIndex); - return index < 0 ? null : itemFillerAt(index); + return index < 0 ? null : itemFillerAt(context, index); } /// Returns the integer range of items corresponding to the [visibleRootIndex]-th root item @@ -306,8 +300,8 @@ class QuestionnaireFillerData extends InheritedWidget { if (rootIndex < 0) return [-1, -1]; final descendantsCount = fillerItemModels - .where((item) => item.rootNode == fillerItemModels[rootIndex]) - .length; + .where((item) => item.rootNode == fillerItemModels[rootIndex]) + .length; return [rootIndex, rootIndex + descendantsCount + 1]; } diff --git a/lib/questionnaires/view/src/questionnaire_page_scaffold.dart b/lib/questionnaires/view/src/questionnaire_page_scaffold.dart index e392c163..3cd4ec87 100644 --- a/lib/questionnaires/view/src/questionnaire_page_scaffold.dart +++ b/lib/questionnaires/view/src/questionnaire_page_scaffold.dart @@ -33,11 +33,10 @@ class DefaultQuestionnairePageScaffoldBuilder @override Widget build( BuildContext context, { - Locale? locale, required void Function(void Function()) setStateCallback, required Widget child, }) { - final theLocale = locale ?? Localizations.localeOf(context); + final theLocale = Localizations.localeOf(context); final questionnaireFiller = QuestionnaireResponseFiller.of(context); final questionnaire = questionnaireFiller diff --git a/lib/questionnaires/view/src/questionnaire_scroller.dart b/lib/questionnaires/view/src/questionnaire_scroller.dart index 7e94a0a0..4c925cc7 100644 --- a/lib/questionnaires/view/src/questionnaire_scroller.dart +++ b/lib/questionnaires/view/src/questionnaire_scroller.dart @@ -26,7 +26,6 @@ import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; /// See: [QuestionnaireScrollerPage] for a [QuestionnaireScroller] which already /// wraps the list in a ready-made [Scaffold], incl. some commonly used buttons. class QuestionnaireScroller extends StatefulWidget { - final Locale? locale; final FhirResourceProvider fhirResourceProvider; final LaunchContext launchContext; final List>? aggregators; @@ -38,7 +37,6 @@ class QuestionnaireScroller extends StatefulWidget { onQuestionnaireResponseChanged; const QuestionnaireScroller({ - this.locale, required this.scaffoldBuilder, required this.fhirResourceProvider, required this.launchContext, @@ -153,12 +151,11 @@ class _QuestionnaireScrollerState extends State { @override Widget build(BuildContext context) { - final locale = widget.locale ?? Localizations.localeOf(context); + final locale = Localizations.localeOf(context); return QuestionnaireResponseFiller( fhirResourceProvider: widget.fhirResourceProvider, launchContext: widget.launchContext, - locale: locale, questionnaireModelDefaults: widget.questionnaireModelDefaults, builder: (BuildContext context) { _belowFillerContext = context; @@ -170,50 +167,46 @@ class _QuestionnaireScrollerState extends State { 'Scroll position: ${_itemPositionsListener.itemPositions.value}', ); - return Localizations.override( - context: context, - locale: locale, - child: widget.scaffoldBuilder.build( - context, - setStateCallback: (fn) { - setState(fn); - }, - child: LayoutBuilder( - builder: (context, constraints) { - const edgeInsets = 8.0; - const twice = 2; - - return ScrollablePositionedList.builder( - itemScrollController: _listScrollController, - itemPositionsListener: _itemPositionsListener, - itemCount: totalLength, - padding: const EdgeInsets.all(edgeInsets), - minCacheExtent: 200, // Allow tabbing to prev/next items - itemBuilder: (BuildContext context, int i) { - return Row( - children: [ - Container( - constraints: BoxConstraints( - maxWidth: QuestionnaireTheme.of(context) - .maxItemWidth - .clamp( - constraints.minWidth, - constraints.maxWidth - twice * edgeInsets, - ), - ), - child: QuestionnaireTheme.of(context).scrollerItemBuilder( - context, - QuestionnaireResponseFiller.of(context), - i, - ), + return widget.scaffoldBuilder.build( + context, + setStateCallback: (fn) { + setState(fn); + }, + child: LayoutBuilder( + builder: (context, constraints) { + const edgeInsets = 8.0; + const twice = 2; + + return ScrollablePositionedList.builder( + itemScrollController: _listScrollController, + itemPositionsListener: _itemPositionsListener, + itemCount: totalLength, + padding: const EdgeInsets.all(edgeInsets), + minCacheExtent: 200, // Allow tabbing to prev/next items + itemBuilder: (BuildContext context, int i) { + return Row( + children: [ + Container( + constraints: BoxConstraints( + maxWidth: + QuestionnaireTheme.of(context).maxItemWidth.clamp( + constraints.minWidth, + constraints.maxWidth - twice * edgeInsets, + ), + ), + child: + QuestionnaireTheme.of(context).scrollerItemBuilder( + context, + QuestionnaireResponseFiller.of(context), + i, ), - const Spacer(), - ], - ); - }, - ); - }, - ), + ), + const Spacer(), + ], + ); + }, + ); + }, ), ); }, diff --git a/lib/questionnaires/view/src/questionnaire_scroller_page.dart b/lib/questionnaires/view/src/questionnaire_scroller_page.dart index c3cd388c..4bbc93ef 100644 --- a/lib/questionnaires/view/src/questionnaire_scroller_page.dart +++ b/lib/questionnaires/view/src/questionnaire_scroller_page.dart @@ -6,7 +6,6 @@ import 'package:flutter/material.dart'; /// /// Fills up the entire page, provides default navigation, help button. class QuestionnaireScrollerPage extends StatelessWidget { - final Locale? locale; final FhirResourceProvider fhirResourceProvider; final LaunchContext launchContext; final Widget? floatingActionButton; @@ -16,7 +15,6 @@ class QuestionnaireScrollerPage extends StatelessWidget { final QuestionnaireModelDefaults questionnaireModelDefaults; const QuestionnaireScrollerPage({ - this.locale, required this.fhirResourceProvider, required this.launchContext, this.floatingActionButton, @@ -30,7 +28,6 @@ class QuestionnaireScrollerPage extends StatelessWidget { @override Widget build(BuildContext context) { return QuestionnaireScroller( - locale: locale, scaffoldBuilder: DefaultQuestionnairePageScaffoldBuilder( // Progress can only be shown instead of a FAB floatingActionButton: floatingActionButton ?? diff --git a/lib/questionnaires/view/src/questionnaire_stepper.dart b/lib/questionnaires/view/src/questionnaire_stepper.dart index 16ebacbf..c2ec8fd1 100644 --- a/lib/questionnaires/view/src/questionnaire_stepper.dart +++ b/lib/questionnaires/view/src/questionnaire_stepper.dart @@ -6,7 +6,6 @@ import 'package:flutter/material.dart'; /// Fill a questionnaire through a wizard-style series of individual questions. class QuestionnaireStepper extends StatefulWidget { - final Locale? locale; final FhirResourceProvider fhirResourceProvider; final LaunchContext launchContext; final QuestionnairePageScaffoldBuilder scaffoldBuilder; @@ -23,7 +22,6 @@ class QuestionnaireStepper extends StatefulWidget { final void Function(FillerItemModel?)? onVisibleItemUpdated; const QuestionnaireStepper({ - this.locale, required this.scaffoldBuilder, required this.fhirResourceProvider, required this.launchContext, @@ -59,7 +57,6 @@ class QuestionnaireStepperState extends State { @override Widget build(BuildContext context) { return QuestionnaireResponseFiller( - locale: widget.locale ?? Localizations.localeOf(context), fhirResourceProvider: widget.fhirResourceProvider, launchContext: widget.launchContext, questionnaireModelDefaults: widget.questionnaireModelDefaults, @@ -95,7 +92,8 @@ class QuestionnaireStepperState extends State { Decimal value, Widget? child, ) { - final scoreString = value.value!.round().toString(); + final scoreString = + value.value!.round().toString(); return AnimatedSwitcher( duration: const Duration(milliseconds: 200), diff --git a/lib/questionnaires/view/src/questionnaire_stepper_page.dart b/lib/questionnaires/view/src/questionnaire_stepper_page.dart index 2107bb7e..4b64ead0 100644 --- a/lib/questionnaires/view/src/questionnaire_stepper_page.dart +++ b/lib/questionnaires/view/src/questionnaire_stepper_page.dart @@ -8,14 +8,12 @@ import 'package:flutter/material.dart'; /// see [QuestionnaireScrollerPage] class QuestionnaireStepperPage extends QuestionnaireStepper { const QuestionnaireStepperPage({ - Locale? locale, required FhirResourceProvider fhirResourceProvider, required LaunchContext launchContext, QuestionnaireModelDefaults questionnaireModelDefaults = const QuestionnaireModelDefaults(), Key? key, }) : super( - locale: locale, scaffoldBuilder: const DefaultQuestionnairePageScaffoldBuilder(), fhirResourceProvider: fhirResourceProvider, launchContext: launchContext, diff --git a/lib/questionnaires/view/src/questionnaire_stepper_page_view.dart b/lib/questionnaires/view/src/questionnaire_stepper_page_view.dart index 26265588..b9d90139 100644 --- a/lib/questionnaires/view/src/questionnaire_stepper_page_view.dart +++ b/lib/questionnaires/view/src/questionnaire_stepper_page_view.dart @@ -30,8 +30,9 @@ class QuestionnaireStepperPageView extends StatefulWidget { _QuestionnaireStepperPageViewState(); } -class _QuestionnaireStepperPageViewState extends State { - PageController _pageController = PageController(); +class _QuestionnaireStepperPageViewState + extends State { + PageController _pageController = PageController(keepPage: true); bool _hasRequestsRunning = false; QuestionnaireItemFiller? _currentQuestionnaireItemFiller; @@ -49,6 +50,7 @@ class _QuestionnaireStepperPageViewState extends State Date: Mon, 2 Oct 2023 16:21:00 +0900 Subject: [PATCH 02/23] Add validation errors --- .../constraint_validation_error.dart | 13 +++++++++++++ .../custom_validation_error.dart | 12 ++++++++++++ .../src/validation_errors/date_time_error.dart | 11 +++++++++++ .../validation_errors/entry_format_error.dart | 13 +++++++++++++ .../src/validation_errors/fhir_path_error.dart | 14 ++++++++++++++ .../src/validation_errors/max_length_error.dart | 12 ++++++++++++ .../src/validation_errors/max_occurs_error.dart | 12 ++++++++++++ .../src/validation_errors/max_size_error.dart | 13 +++++++++++++ .../src/validation_errors/max_value_error.dart | 12 ++++++++++++ .../src/validation_errors/mime_types_error.dart | 12 ++++++++++++ .../src/validation_errors/min_length_error.dart | 12 ++++++++++++ .../src/validation_errors/min_occurs_error.dart | 12 ++++++++++++ .../src/validation_errors/min_value_error.dart | 12 ++++++++++++ .../model/src/validation_errors/nan_error.dart | 11 +++++++++++ .../src/validation_errors/regex_error.dart | 11 +++++++++++ .../validation_errors/required_item_error.dart | 11 +++++++++++ .../single_selection_or_open_string_error.dart | 17 +++++++++++++++++ .../model/src/validation_errors/url_error.dart | 11 +++++++++++ .../src/validation_errors/validation_error.dart | 9 +++++++++ 19 files changed, 230 insertions(+) create mode 100644 lib/questionnaires/model/src/validation_errors/constraint_validation_error.dart create mode 100644 lib/questionnaires/model/src/validation_errors/custom_validation_error.dart create mode 100644 lib/questionnaires/model/src/validation_errors/date_time_error.dart create mode 100644 lib/questionnaires/model/src/validation_errors/entry_format_error.dart create mode 100644 lib/questionnaires/model/src/validation_errors/fhir_path_error.dart create mode 100644 lib/questionnaires/model/src/validation_errors/max_length_error.dart create mode 100644 lib/questionnaires/model/src/validation_errors/max_occurs_error.dart create mode 100644 lib/questionnaires/model/src/validation_errors/max_size_error.dart create mode 100644 lib/questionnaires/model/src/validation_errors/max_value_error.dart create mode 100644 lib/questionnaires/model/src/validation_errors/mime_types_error.dart create mode 100644 lib/questionnaires/model/src/validation_errors/min_length_error.dart create mode 100644 lib/questionnaires/model/src/validation_errors/min_occurs_error.dart create mode 100644 lib/questionnaires/model/src/validation_errors/min_value_error.dart create mode 100644 lib/questionnaires/model/src/validation_errors/nan_error.dart create mode 100644 lib/questionnaires/model/src/validation_errors/regex_error.dart create mode 100644 lib/questionnaires/model/src/validation_errors/required_item_error.dart create mode 100644 lib/questionnaires/model/src/validation_errors/single_selection_or_open_string_error.dart create mode 100644 lib/questionnaires/model/src/validation_errors/url_error.dart create mode 100644 lib/questionnaires/model/src/validation_errors/validation_error.dart diff --git a/lib/questionnaires/model/src/validation_errors/constraint_validation_error.dart b/lib/questionnaires/model/src/validation_errors/constraint_validation_error.dart new file mode 100644 index 00000000..c4106738 --- /dev/null +++ b/lib/questionnaires/model/src/validation_errors/constraint_validation_error.dart @@ -0,0 +1,13 @@ +import 'package:faiadashu/l10n/src/fdash_localizations.g.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; + +class ConstraintValidationError extends ValidationError { + final String? message; + const ConstraintValidationError(String nodeUid, this.message) + : super(nodeUid); + + @override + String? getMessage(FDashLocalizations localizations) { + return message; + } +} diff --git a/lib/questionnaires/model/src/validation_errors/custom_validation_error.dart b/lib/questionnaires/model/src/validation_errors/custom_validation_error.dart new file mode 100644 index 00000000..1351d41f --- /dev/null +++ b/lib/questionnaires/model/src/validation_errors/custom_validation_error.dart @@ -0,0 +1,12 @@ +import 'package:faiadashu/l10n/src/fdash_localizations.g.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; + +class CustomValidationError extends ValidationError { + final String? message; + const CustomValidationError(String nodeUid, this.message) : super(nodeUid); + + @override + String? getMessage(FDashLocalizations localizations) { + return message!; + } +} diff --git a/lib/questionnaires/model/src/validation_errors/date_time_error.dart b/lib/questionnaires/model/src/validation_errors/date_time_error.dart new file mode 100644 index 00000000..55f8b6f0 --- /dev/null +++ b/lib/questionnaires/model/src/validation_errors/date_time_error.dart @@ -0,0 +1,11 @@ +import 'package:faiadashu/l10n/src/fdash_localizations.g.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; + +class DateTimeError extends ValidationError { + DateTimeError(super.nodeUid); + + @override + String? getMessage(FDashLocalizations localizations) { + return localizations.validatorDateTime; + } +} diff --git a/lib/questionnaires/model/src/validation_errors/entry_format_error.dart b/lib/questionnaires/model/src/validation_errors/entry_format_error.dart new file mode 100644 index 00000000..c000bf54 --- /dev/null +++ b/lib/questionnaires/model/src/validation_errors/entry_format_error.dart @@ -0,0 +1,13 @@ +import 'package:faiadashu/l10n/src/fdash_localizations.g.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; + +class EntryFormatError extends ValidationError { + final String entryFormat; + + const EntryFormatError(String nodeUid, this.entryFormat) : super(nodeUid); + + @override + String? getMessage(FDashLocalizations localizations) { + return localizations.validatorEntryFormat(entryFormat); + } +} diff --git a/lib/questionnaires/model/src/validation_errors/fhir_path_error.dart b/lib/questionnaires/model/src/validation_errors/fhir_path_error.dart new file mode 100644 index 00000000..52a889ab --- /dev/null +++ b/lib/questionnaires/model/src/validation_errors/fhir_path_error.dart @@ -0,0 +1,14 @@ +import 'package:faiadashu/l10n/src/fdash_localizations.g.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; +import 'package:fhir_path/fhir_path.dart'; + +class FhirPathError extends ValidationError { + final FhirPathEvaluationException exception; + + const FhirPathError(String nodeUid, this.exception) : super(nodeUid); + + @override + String? getMessage(FDashLocalizations localizations) { + return exception.message; + } +} diff --git a/lib/questionnaires/model/src/validation_errors/max_length_error.dart b/lib/questionnaires/model/src/validation_errors/max_length_error.dart new file mode 100644 index 00000000..557fd317 --- /dev/null +++ b/lib/questionnaires/model/src/validation_errors/max_length_error.dart @@ -0,0 +1,12 @@ +import 'package:faiadashu/l10n/src/fdash_localizations.g.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; + +class MaxLengthError extends ValidationError { + final int maxLength; + MaxLengthError(String nodeUid, this.maxLength) : super(nodeUid); + + @override + String? getMessage(FDashLocalizations localizations) { + return localizations.validatorMaxLength(maxLength); + } +} diff --git a/lib/questionnaires/model/src/validation_errors/max_occurs_error.dart b/lib/questionnaires/model/src/validation_errors/max_occurs_error.dart new file mode 100644 index 00000000..894ed9d1 --- /dev/null +++ b/lib/questionnaires/model/src/validation_errors/max_occurs_error.dart @@ -0,0 +1,12 @@ +import 'package:faiadashu/l10n/src/fdash_localizations.g.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; + +class MaxOccursError extends ValidationError { + final int maxOccurs; + MaxOccursError(String nodeUid, this.maxOccurs) : super(nodeUid); + + @override + String? getMessage(FDashLocalizations localizations) { + return localizations.validatorMaxOccurs(maxOccurs); + } +} diff --git a/lib/questionnaires/model/src/validation_errors/max_size_error.dart b/lib/questionnaires/model/src/validation_errors/max_size_error.dart new file mode 100644 index 00000000..226608c0 --- /dev/null +++ b/lib/questionnaires/model/src/validation_errors/max_size_error.dart @@ -0,0 +1,13 @@ +import 'package:faiadashu/l10n/src/fdash_localizations.g.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; +import 'package:filesize/filesize.dart'; + +class MaxSizeError extends ValidationError { + final size; + MaxSizeError(String nodeUid, this.size) : super(nodeUid); + + @override + String? getMessage(FDashLocalizations localizations) { + return localizations.validatorMaxSize(filesize(size)); + } +} diff --git a/lib/questionnaires/model/src/validation_errors/max_value_error.dart b/lib/questionnaires/model/src/validation_errors/max_value_error.dart new file mode 100644 index 00000000..387c2067 --- /dev/null +++ b/lib/questionnaires/model/src/validation_errors/max_value_error.dart @@ -0,0 +1,12 @@ +import 'package:faiadashu/l10n/src/fdash_localizations.g.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; + +class MaxValueError extends ValidationError { + final String maxValue; + MaxValueError(String nodeUid, this.maxValue) : super(nodeUid); + + @override + String? getMessage(FDashLocalizations localizations) { + return localizations.validatorMaxValue(maxValue); + } +} diff --git a/lib/questionnaires/model/src/validation_errors/mime_types_error.dart b/lib/questionnaires/model/src/validation_errors/mime_types_error.dart new file mode 100644 index 00000000..9b4b098a --- /dev/null +++ b/lib/questionnaires/model/src/validation_errors/mime_types_error.dart @@ -0,0 +1,12 @@ +import 'package:faiadashu/l10n/src/fdash_localizations.g.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; + +class MimeTypesError extends ValidationError { + final List mimeTypes; + MimeTypesError(String nodeUid, this.mimeTypes) : super(nodeUid); + + @override + String? getMessage(FDashLocalizations localizations) { + return localizations.validatorMimeTypes(mimeTypes.join(",")); + } +} diff --git a/lib/questionnaires/model/src/validation_errors/min_length_error.dart b/lib/questionnaires/model/src/validation_errors/min_length_error.dart new file mode 100644 index 00000000..f910f575 --- /dev/null +++ b/lib/questionnaires/model/src/validation_errors/min_length_error.dart @@ -0,0 +1,12 @@ +import 'package:faiadashu/l10n/src/fdash_localizations.g.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; + +class MinLengthError extends ValidationError { + final int minLength; + MinLengthError(String nodeUid, this.minLength) : super(nodeUid); + + @override + String? getMessage(FDashLocalizations localizations) { + return localizations.validatorMinLength(minLength); + } +} diff --git a/lib/questionnaires/model/src/validation_errors/min_occurs_error.dart b/lib/questionnaires/model/src/validation_errors/min_occurs_error.dart new file mode 100644 index 00000000..68c1bb23 --- /dev/null +++ b/lib/questionnaires/model/src/validation_errors/min_occurs_error.dart @@ -0,0 +1,12 @@ +import 'package:faiadashu/l10n/src/fdash_localizations.g.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; + +class MinOccursError extends ValidationError { + final int minOccurs; + MinOccursError(String nodeUid, this.minOccurs) : super(nodeUid); + + @override + String? getMessage(FDashLocalizations localizations) { + return localizations.validatorMinOccurs(minOccurs); + } +} diff --git a/lib/questionnaires/model/src/validation_errors/min_value_error.dart b/lib/questionnaires/model/src/validation_errors/min_value_error.dart new file mode 100644 index 00000000..bc6cefc9 --- /dev/null +++ b/lib/questionnaires/model/src/validation_errors/min_value_error.dart @@ -0,0 +1,12 @@ +import 'package:faiadashu/l10n/src/fdash_localizations.g.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; + +class MinValueError extends ValidationError { + final String minValue; + MinValueError(String nodeUid, this.minValue) : super(nodeUid); + + @override + String? getMessage(FDashLocalizations localizations) { + return localizations.validatorMinValue(minValue); + } +} diff --git a/lib/questionnaires/model/src/validation_errors/nan_error.dart b/lib/questionnaires/model/src/validation_errors/nan_error.dart new file mode 100644 index 00000000..114cb164 --- /dev/null +++ b/lib/questionnaires/model/src/validation_errors/nan_error.dart @@ -0,0 +1,11 @@ +import 'package:faiadashu/l10n/src/fdash_localizations.g.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; + +class NanError extends ValidationError { + NanError(super.nodeUid); + + @override + String? getMessage(FDashLocalizations localizations) { + return localizations.validatorNan; + } +} diff --git a/lib/questionnaires/model/src/validation_errors/regex_error.dart b/lib/questionnaires/model/src/validation_errors/regex_error.dart new file mode 100644 index 00000000..5b6451ba --- /dev/null +++ b/lib/questionnaires/model/src/validation_errors/regex_error.dart @@ -0,0 +1,11 @@ +import 'package:faiadashu/l10n/src/fdash_localizations.g.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; + +class RegexError extends ValidationError { + RegexError(super.nodeUid); + + @override + String? getMessage(FDashLocalizations localizations) { + return localizations.validatorRegExp; + } +} diff --git a/lib/questionnaires/model/src/validation_errors/required_item_error.dart b/lib/questionnaires/model/src/validation_errors/required_item_error.dart new file mode 100644 index 00000000..634992d9 --- /dev/null +++ b/lib/questionnaires/model/src/validation_errors/required_item_error.dart @@ -0,0 +1,11 @@ +import 'package:faiadashu/l10n/src/fdash_localizations.g.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; + +class RequiredItemError extends ValidationError { + RequiredItemError(super.nodeUid); + + @override + String? getMessage(FDashLocalizations localizations) { + return localizations.validatorRequiredItem; + } +} diff --git a/lib/questionnaires/model/src/validation_errors/single_selection_or_open_string_error.dart b/lib/questionnaires/model/src/validation_errors/single_selection_or_open_string_error.dart new file mode 100644 index 00000000..aee98d8b --- /dev/null +++ b/lib/questionnaires/model/src/validation_errors/single_selection_or_open_string_error.dart @@ -0,0 +1,17 @@ +import 'package:faiadashu/l10n/src/fdash_localizations.g.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; + +class SingleSelectionOrOpenStringError extends ValidationError { + final String? openLabel; + SingleSelectionOrOpenStringError( + String nodeUid, + this.openLabel, + ) : super(nodeUid); + + @override + String? getMessage(FDashLocalizations localizations) { + return localizations.validatorSingleSelectionOrSingleOpenString( + openLabel ?? localizations.fillerOpenCodingOtherLabel, + ); + } +} diff --git a/lib/questionnaires/model/src/validation_errors/url_error.dart b/lib/questionnaires/model/src/validation_errors/url_error.dart new file mode 100644 index 00000000..e84dc17d --- /dev/null +++ b/lib/questionnaires/model/src/validation_errors/url_error.dart @@ -0,0 +1,11 @@ +import 'package:faiadashu/l10n/src/fdash_localizations.g.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; + +class UrlError extends ValidationError { + UrlError(super.nodeUid); + + @override + String? getMessage(FDashLocalizations localizations) { + return localizations.validatorUrl; + } +} diff --git a/lib/questionnaires/model/src/validation_errors/validation_error.dart b/lib/questionnaires/model/src/validation_errors/validation_error.dart new file mode 100644 index 00000000..46b50acd --- /dev/null +++ b/lib/questionnaires/model/src/validation_errors/validation_error.dart @@ -0,0 +1,9 @@ +import 'package:faiadashu/l10n/l10n.dart'; + +abstract class ValidationError { + final String nodeUid; + + const ValidationError(this.nodeUid); + + String? getMessage(FDashLocalizations localizations); +} From 77d97d371b6bc9c7c0a026c1ea6cf41331ce9de8 Mon Sep 17 00:00:00 2001 From: Hanny Prastya Hariyadi Date: Mon, 2 Oct 2023 19:21:58 +0900 Subject: [PATCH 03/23] Update example --- .../custom_questionnaire_stepper_page.dart | 45 ++++- example/lib/main.dart | 99 +++++++--- example/lib/questionnaire_launch_tile.dart | 174 +++++++++--------- 3 files changed, 203 insertions(+), 115 deletions(-) diff --git a/example/lib/custom_questionnaire_stepper_page.dart b/example/lib/custom_questionnaire_stepper_page.dart index cc1d9910..e18513a8 100644 --- a/example/lib/custom_questionnaire_stepper_page.dart +++ b/example/lib/custom_questionnaire_stepper_page.dart @@ -1,4 +1,6 @@ import 'package:faiadashu/faiadashu.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; +import 'package:faiadashu_example/main.dart'; import 'package:flutter/material.dart'; class CustomQuestionnaireStepperPage extends StatefulWidget { @@ -40,9 +42,11 @@ class _CustomQuestionnaireStepperPageState return; } - // Validate the first matching item, and if it's valid, navigate to the next page - if (matchingItems.first.validate(notifyListeners: true) == null) { + try { + matchingItems.first.validate(notifyListeners: true); _navigateToNextPage(); + } on ValidationError { + rethrow; } } @@ -67,8 +71,39 @@ class _CustomQuestionnaireStepperPageState children: [ Expanded( child: QuestionnaireStepper( - scaffoldBuilder: - const DefaultQuestionnairePageScaffoldBuilder(), + scaffoldBuilder: DefaultQuestionnairePageScaffoldBuilder( + persistentFooterButtons: [ + PopupMenuButton( + icon: const Icon(Icons.language), + onSelected: (Locale locale) { + LocaleInheritedWidget.of(context) + .updateLocale(locale); + }, + itemBuilder: (BuildContext context) => + >[ + const PopupMenuItem( + value: Locale('en'), + child: Text('English'), + ), + const PopupMenuItem( + value: Locale('ar'), + child: Text('عَرَبِيّ'), + ), + const PopupMenuItem( + value: Locale('de'), + child: Text('Deutsch'), + ), + const PopupMenuItem( + value: Locale('es'), + child: Text('Español'), + ), + const PopupMenuItem( + value: Locale('ja'), + child: Text('日本語'), + ), + ], + ), + ]), fhirResourceProvider: widget.fhirResourceProvider, launchContext: widget.launchContext, controller: _controller, @@ -77,8 +112,6 @@ class _CustomQuestionnaireStepperPageState }, onPageChanged: _onPageChanged, onBeforePageChanged: (currentItemModel, nextItemModel) async { - /// Adding delay - await Future.delayed(Duration(seconds: 1)); return BeforePageChangedData(canProceed: true); }, onVisibleItemUpdated: (item) { diff --git a/example/lib/main.dart b/example/lib/main.dart index 0019d3c9..a37fd3cf 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -44,32 +44,73 @@ void main() { runApp(const MyApp()); } -class MyApp extends StatelessWidget { +class MyApp extends StatefulWidget { const MyApp({Key? key}) : super(key: key); + @override + MyAppState createState() => MyAppState(); +} + +class MyAppState extends State { + Locale _currentLocale = const Locale('en', 'US'); + + void _updateLocale(Locale newLocale) { + setState(() { + _currentLocale = newLocale; + }); + } + @override Widget build(BuildContext context) { - return MaterialApp( - debugShowCheckedModeBanner: false, - theme: ThemeData.light(useMaterial3: true).copyWith( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.pink), - inputDecorationTheme: ThemeData.light(useMaterial3: true).inputDecorationTheme.copyWith( - filled: true, - ), + return LocaleInheritedWidget( + updateLocale: _updateLocale, + currentLocale: _currentLocale, + child: MaterialApp( + debugShowCheckedModeBanner: false, + theme: ThemeData.light(useMaterial3: true).copyWith( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.pink), + inputDecorationTheme: + ThemeData.light(useMaterial3: true).inputDecorationTheme.copyWith( + filled: true, + ), + ), + darkTheme: ThemeData.dark(useMaterial3: true), + title: 'Faiadashu™ FHIRDash Gallery', + localizationsDelegates: const [ + FDashLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: FDashLocalizations.supportedLocales, + locale: _currentLocale, + home: const HomePage(), ), - darkTheme: ThemeData.dark(useMaterial3: true), - title: 'Faiadashu™ FHIRDash Gallery', - localizationsDelegates: const [ - FDashLocalizations.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - ], - supportedLocales: FDashLocalizations.supportedLocales, - home: const HomePage(), ); } } +class LocaleInheritedWidget extends InheritedWidget { + final Function(Locale) updateLocale; + final Locale currentLocale; + + const LocaleInheritedWidget({ + Key? key, + required this.updateLocale, + required this.currentLocale, + required Widget child, + }) : super(key: key, child: child); + + @override + bool updateShouldNotify(LocaleInheritedWidget old) { + return old.currentLocale != currentLocale; + } + + static LocaleInheritedWidget of(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType()!; + } +} + class HomePage extends StatefulWidget { const HomePage({Key? key}) : super(key: key); @@ -123,7 +164,6 @@ class _HomePageState extends State { clientId: '4564f6f7-335f-43d3-8867-a0f4e6f901d6', redirectUri: FhirUri('com.legentix.faiagallery://callback'), ); - } @override @@ -134,7 +174,8 @@ class _HomePageState extends State { /// Schedules repaint after login / logout. void _onLoginChanged() { - _logger.debug('_onLoginChanged: ${questionnaireResponseStorage.smartClient.isLoggedIn()}'); + _logger.debug( + '_onLoginChanged: ${questionnaireResponseStorage.smartClient.isLoggedIn()}'); setState(() { // Rebuild }); @@ -206,7 +247,8 @@ class _HomePageState extends State { ), ), actions: [ - SmartLoginButton(questionnaireResponseStorage.smartClient, onLoginChanged: _onLoginChanged) + SmartLoginButton(questionnaireResponseStorage.smartClient, + onLoginChanged: _onLoginChanged) ], ), body: SafeArea( @@ -225,7 +267,8 @@ class _HomePageState extends State { launchContext: launchContext, questionnairePath: 'assets/instruments/beverage_ig.json', saveResponseFunction: questionnaireResponseStorage.saveToMemory, - restoreResponseFunction: questionnaireResponseStorage.restoreFromMemory, + restoreResponseFunction: + questionnaireResponseStorage.restoreFromMemory, uploadResponseFunction: uploadResponseFunction, questionnaireModelDefaults: QuestionnaireModelDefaults( prefixBuilder: (fim) { @@ -358,7 +401,7 @@ class _HomePageState extends State { child: CustomQuestionnaireStepperPage( fhirResourceProvider: AssetResourceProvider.singleton( questionnaireResourceUri, - 'assets/instruments/framingham-hcdc.json', + 'assets/instruments/triage.json', ), launchContext: launchContext, ), @@ -396,7 +439,8 @@ class _HomePageState extends State { launchContext: launchContext, questionnairePath: 'assets/instruments/bluebook.json', saveResponseFunction: questionnaireResponseStorage.saveToMemory, - restoreResponseFunction: questionnaireResponseStorage.restoreFromMemory, + restoreResponseFunction: + questionnaireResponseStorage.restoreFromMemory, uploadResponseFunction: uploadResponseFunction, ), _launchQuestionnaire( @@ -425,7 +469,8 @@ class _HomePageState extends State { launchContext: launchContext, questionnairePath: 'assets/instruments/argonaut_sampler.json', saveResponseFunction: questionnaireResponseStorage.saveToMemory, - restoreResponseFunction: questionnaireResponseStorage.restoreFromMemory, + restoreResponseFunction: + questionnaireResponseStorage.restoreFromMemory, uploadResponseFunction: uploadResponseFunction, ), QuestionnaireLaunchTile( @@ -439,7 +484,8 @@ class _HomePageState extends State { launchContext: launchContext, questionnairePath: 'assets/instruments/argonaut_sampler.json', saveResponseFunction: questionnaireResponseStorage.saveToMemory, - restoreResponseFunction: questionnaireResponseStorage.restoreFromMemory, + restoreResponseFunction: + questionnaireResponseStorage.restoreFromMemory, uploadResponseFunction: uploadResponseFunction, ), QuestionnaireLaunchTile( @@ -453,7 +499,8 @@ class _HomePageState extends State { launchContext: launchContext, questionnairePath: 'assets/instruments/argonaut_sampler.json', saveResponseFunction: questionnaireResponseStorage.saveToMemory, - restoreResponseFunction: questionnaireResponseStorage.restoreFromMemory, + restoreResponseFunction: + questionnaireResponseStorage.restoreFromMemory, uploadResponseFunction: uploadResponseFunction, ), _headline( diff --git a/example/lib/questionnaire_launch_tile.dart b/example/lib/questionnaire_launch_tile.dart index d8f04e96..cbfe0cec 100644 --- a/example/lib/questionnaire_launch_tile.dart +++ b/example/lib/questionnaire_launch_tile.dart @@ -15,11 +15,12 @@ class QuestionnaireLaunchTile extends StatefulWidget { final Locale? locale; final FhirResourceProvider fhirResourceProvider; final LaunchContext launchContext; - final void Function(String questionnairePath, QuestionnaireResponse? questionnaireResponse) - saveResponseFunction; - final void Function(BuildContext context, String questionnairePath, QuestionnaireResponse? questionnaireResponse)? - uploadResponseFunction; - final QuestionnaireResponse? Function(String questionnairePath) restoreResponseFunction; + final void Function(String questionnairePath, + QuestionnaireResponse? questionnaireResponse) saveResponseFunction; + final void Function(BuildContext context, String questionnairePath, + QuestionnaireResponse? questionnaireResponse)? uploadResponseFunction; + final QuestionnaireResponse? Function(String questionnairePath) + restoreResponseFunction; final QuestionnaireModelDefaults questionnaireModelDefaults; @@ -45,7 +46,6 @@ class QuestionnaireLaunchTile extends StatefulWidget { class _QuestionnaireLaunchTileState extends State { late final FhirResourceProvider _questionnaireProvider; - late Locale _locale; late NumberFormat _percentPattern; late Future _modelFuture; @@ -58,11 +58,13 @@ class _QuestionnaireLaunchTileState extends State { ); } - Future _createModelFuture() { + Future _createModelFuture( + FDashLocalizations localizations) { return QuestionnaireResponseModel.fromFhirResourceBundle( + localizations: localizations, fhirResourceProvider: _questionnaireProvider, launchContext: widget.launchContext, - locale: _locale, + locale: Locale('ja'), ).then((qrm) { qrm.populate( widget.restoreResponseFunction.call(widget.questionnairePath), @@ -74,9 +76,10 @@ class _QuestionnaireLaunchTileState extends State { @override void didChangeDependencies() { super.didChangeDependencies(); - _locale = widget.locale ?? Localizations.localeOf(context); - _percentPattern = NumberFormat.percentPattern(_locale.toString()); - _modelFuture = _createModelFuture(); + _percentPattern = NumberFormat.percentPattern( + Localizations.localeOf(context).toString(), + ); + _modelFuture = _createModelFuture(FDashLocalizations.of(context)); } @override @@ -134,62 +137,92 @@ class _QuestionnaireLaunchTileState extends State { Navigator.push( context, MaterialPageRoute( - builder: (context) => QuestionnaireScrollerPage( - locale: _locale, - fhirResourceProvider: RegistryFhirResourceProvider([ - AssetResourceProvider.singleton( - questionnaireResourceUri, - widget.questionnairePath, - ), - InMemoryResourceProvider.inMemory( - questionnaireResponseResourceUri, - widget.restoreResponseFunction( + builder: (context) => Localizations.override( + context: context, + locale: widget.locale, + child: QuestionnaireScrollerPage( + fhirResourceProvider: RegistryFhirResourceProvider([ + AssetResourceProvider.singleton( + questionnaireResourceUri, widget.questionnairePath, ), - ), - widget.fhirResourceProvider, - ]), - launchContext: widget.launchContext, - // Callback for supportLink - onLinkTap: launchLink, - persistentFooterButtons: [ - Builder( - builder: (context) => const QuestionnaireCompleteButton(), - ), - if (widget.uploadResponseFunction != null) + InMemoryResourceProvider.inMemory( + questionnaireResponseResourceUri, + widget.restoreResponseFunction( + widget.questionnairePath, + ), + ), + widget.fhirResourceProvider, + ]), + launchContext: widget.launchContext, + // Callback for supportLink + onLinkTap: launchLink, + persistentFooterButtons: [ + Builder( + builder: (context) => const QuestionnaireCompleteButton(), + ), + if (widget.uploadResponseFunction != null) + Builder( + builder: (context) => ElevatedButton.icon( + label: Text( + FDashLocalizations.of(context) + .handlingUploadButtonLabel, + ), + icon: const Icon(Icons.cloud_upload), + onPressed: () { + // Generate a response and upload it to a FHIR server. + // In a real-world scenario this would have more robust state handling. + widget.uploadResponseFunction?.call( + context, + widget.questionnairePath, + QuestionnaireResponseFiller.of(context) + .aggregator() + .aggregate( + responseStatus: + QuestionnaireResponseStatus.completed, + ), + ); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + FDashLocalizations.of(context) + .handlingUploading, + ), + SyncIndicator( + color: + Theme.of(context).colorScheme.primary, + ) + ], + ), + ), + ); + Navigator.pop(context); + }, + ), + ), Builder( builder: (context) => ElevatedButton.icon( label: Text( - FDashLocalizations.of(context) - .handlingUploadButtonLabel, + FDashLocalizations.of(context).handlingSaveButtonLabel, ), - icon: const Icon(Icons.cloud_upload), + icon: const Icon(Icons.save_alt), onPressed: () { - // Generate a response and upload it to a FHIR server. - // In a real-world scenario this would have more robust state handling. - widget.uploadResponseFunction?.call( - context, + // Generate a response and store it in-memory. + // In a real-world scenario one would persist or post the response instead. + widget.saveResponseFunction.call( widget.questionnairePath, QuestionnaireResponseFiller.of(context) .aggregator() - .aggregate( - responseStatus: - QuestionnaireResponseStatus.completed, - ), + .aggregate(), ); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - FDashLocalizations.of(context) - .handlingUploading, - ), - SyncIndicator( - color: Theme.of(context).colorScheme.primary, - ) - ], + content: Text( + FDashLocalizations.of(context).handlingSaved, ), ), ); @@ -197,40 +230,15 @@ class _QuestionnaireLaunchTileState extends State { }, ), ), - Builder( - builder: (context) => ElevatedButton.icon( - label: Text( - FDashLocalizations.of(context).handlingSaveButtonLabel, - ), - icon: const Icon(Icons.save_alt), - onPressed: () { - // Generate a response and store it in-memory. - // In a real-world scenario one would persist or post the response instead. - widget.saveResponseFunction.call( - widget.questionnairePath, - QuestionnaireResponseFiller.of(context) - .aggregator() - .aggregate(), - ); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - FDashLocalizations.of(context).handlingSaved, - ), - ), - ); - Navigator.pop(context); - }, - ), - ), - ], - questionnaireModelDefaults: widget.questionnaireModelDefaults, + ], + questionnaireModelDefaults: widget.questionnaireModelDefaults, + ), ), ), ).then((value) { // This triggers after return from questionnaire filler setState(() { - _modelFuture = _createModelFuture(); + _modelFuture = _createModelFuture(FDashLocalizations.of(context)); }); }); }, From 38b0c1d334e36cd5ce423b3ec29f9b90a4e4beb3 Mon Sep 17 00:00:00 2001 From: Hanny Prastya Hariyadi Date: Mon, 2 Oct 2023 19:23:56 +0900 Subject: [PATCH 04/23] Fix prefix not properly rendered --- .../view/item/src/questionnaire_item_filler_title.dart | 2 +- lib/questionnaires/view/src/questionnaire_scroller.dart | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/questionnaires/view/item/src/questionnaire_item_filler_title.dart b/lib/questionnaires/view/item/src/questionnaire_item_filler_title.dart index cb8d2015..95057a6b 100644 --- a/lib/questionnaires/view/item/src/questionnaire_item_filler_title.dart +++ b/lib/questionnaires/view/item/src/questionnaire_item_filler_title.dart @@ -56,7 +56,7 @@ class QuestionnaireItemFillerTitle extends StatelessWidget { ); final htmlTitleText = (prefixText != null) - ? '$openStyleTag${prefixText} ${title}$requiredTag$closeStyleTag' + ? '$openStyleTag${prefixText.xhtmlText} ${title}$requiredTag$closeStyleTag' : '$openStyleTag${title}$requiredTag$closeStyleTag'; return QuestionnaireItemFillerTitle._( diff --git a/lib/questionnaires/view/src/questionnaire_scroller.dart b/lib/questionnaires/view/src/questionnaire_scroller.dart index 4c925cc7..3b9d7698 100644 --- a/lib/questionnaires/view/src/questionnaire_scroller.dart +++ b/lib/questionnaires/view/src/questionnaire_scroller.dart @@ -151,8 +151,6 @@ class _QuestionnaireScrollerState extends State { @override Widget build(BuildContext context) { - final locale = Localizations.localeOf(context); - return QuestionnaireResponseFiller( fhirResourceProvider: widget.fhirResourceProvider, launchContext: widget.launchContext, From 5c40a463474fd8c4174ffa06c649b3ae25f11604 Mon Sep 17 00:00:00 2001 From: Hanny Prastya Hariyadi Date: Mon, 2 Oct 2023 19:27:43 +0900 Subject: [PATCH 05/23] Remove unintended changes --- lib/questionnaires/model/src/questionnaire_response_model.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/questionnaires/model/src/questionnaire_response_model.dart b/lib/questionnaires/model/src/questionnaire_response_model.dart index f53dd19b..fb83cc9b 100644 --- a/lib/questionnaires/model/src/questionnaire_response_model.dart +++ b/lib/questionnaires/model/src/questionnaire_response_model.dart @@ -826,7 +826,6 @@ class QuestionnaireResponseModel { /// Will return the parent [QuestionItemModel] if uid corresponds to an answer. FillerItemModel? fillerItemModelByUid(String uid) { final fillerItem = orderedFillerItemModels().firstWhereOrNull((fim) { - print("UID:" + fim.nodeUid); return fim.nodeUid == uid; }); if (fillerItem != null) { From 762b5737aca7ee13581d544baf2a4a4eb21fab84 Mon Sep 17 00:00:00 2001 From: Hanny Prastya Hariyadi Date: Mon, 2 Oct 2023 19:38:13 +0900 Subject: [PATCH 06/23] Remove Locale from any widget --- lib/fhir_types/src/codeable_concept_text.dart | 5 +---- lib/fhir_types/src/date_time_picker.dart | 2 -- lib/fhir_types/src/date_time_text.dart | 5 +---- lib/observations/src/observation_value_view.dart | 4 +--- lib/observations/src/observation_view.dart | 4 ---- .../view/item/answer/src/datetime_answer_filler.dart | 1 - 6 files changed, 3 insertions(+), 18 deletions(-) diff --git a/lib/fhir_types/src/codeable_concept_text.dart b/lib/fhir_types/src/codeable_concept_text.dart index 495a72a1..0e0e5b29 100644 --- a/lib/fhir_types/src/codeable_concept_text.dart +++ b/lib/fhir_types/src/codeable_concept_text.dart @@ -5,20 +5,17 @@ import 'package:flutter/material.dart'; class CodeableConceptText extends StatelessWidget { final CodeableConcept codeableConcept; final TextStyle? style; - final Locale? locale; const CodeableConceptText( this.codeableConcept, { this.style, - this.locale, Key? key, }) : super(key: key); @override Widget build(BuildContext context) { return Text( - codeableConcept - .localizedDisplay(locale ?? Localizations.localeOf(context)), + codeableConcept.localizedDisplay(Localizations.localeOf(context)), style: style, ); } diff --git a/lib/fhir_types/src/date_time_picker.dart b/lib/fhir_types/src/date_time_picker.dart index 23b0ad0e..d84139f9 100644 --- a/lib/fhir_types/src/date_time_picker.dart +++ b/lib/fhir_types/src/date_time_picker.dart @@ -10,7 +10,6 @@ import 'package:intl/intl.dart'; /// The control is displayed as a text field that can be tapped to open /// a picker. class FhirDateTimePicker extends StatefulWidget { - final Locale? locale; final DateTime firstDate; final DateTime lastDate; final FhirDateTime? initialDateTime; @@ -31,7 +30,6 @@ class FhirDateTimePicker extends StatefulWidget { this.timePickerEntryMode = TimePickerEntryMode.dial, this.decoration, this.onChanged, - this.locale, this.focusNode, this.enabled = true, Key? key, diff --git a/lib/fhir_types/src/date_time_text.dart b/lib/fhir_types/src/date_time_text.dart index 703c471a..419cb8e8 100644 --- a/lib/fhir_types/src/date_time_text.dart +++ b/lib/fhir_types/src/date_time_text.dart @@ -8,20 +8,17 @@ class FhirDateTimeText extends StatelessWidget { final FhirDateTime? dateTime; final TextStyle? style; final String defaultText; - final Locale? locale; const FhirDateTimeText( this.dateTime, { this.style, this.defaultText = '', - this.locale, Key? key, }) : super(key: key); @override Widget build(BuildContext context) { return Text( - dateTime?.format(locale ?? Localizations.localeOf(context)) ?? - defaultText, + dateTime?.format(Localizations.localeOf(context)) ?? defaultText, style: style, ); } diff --git a/lib/observations/src/observation_value_view.dart b/lib/observations/src/observation_value_view.dart index e634b783..0fc71666 100644 --- a/lib/observations/src/observation_value_view.dart +++ b/lib/observations/src/observation_value_view.dart @@ -10,7 +10,6 @@ import 'package:intl/intl.dart'; /// or components of valueQuantity. class ObservationValueView extends StatelessWidget { final Observation _observation; - final Locale? locale; final TextStyle? valueStyle; final TextStyle? unitStyle; final String componentSeparator; @@ -20,7 +19,6 @@ class ObservationValueView extends StatelessWidget { const ObservationValueView( this._observation, { super.key, - this.locale, this.valueStyle, this.unitStyle, this.componentSeparator = ' | ', @@ -33,7 +31,7 @@ class ObservationValueView extends StatelessWidget { final Widget valueWidget; final decimalFormat = NumberFormat.decimalPattern( - (locale ?? Localizations.localeOf(context)).toString(), + (Localizations.localeOf(context)).toString(), ); if (_observation.valueQuantity != null) { diff --git a/lib/observations/src/observation_view.dart b/lib/observations/src/observation_view.dart index 2b544493..8758a2e0 100644 --- a/lib/observations/src/observation_view.dart +++ b/lib/observations/src/observation_view.dart @@ -11,12 +11,10 @@ class ObservationView extends StatelessWidget { final Widget _valueWidget; final Widget _codeWidget; final Widget _dateTimeWidget; - final Locale? locale; ObservationView( Observation observation, { super.key, - this.locale, TextStyle? valueStyle, TextStyle? unitStyle, TextStyle? codeStyle, @@ -28,7 +26,6 @@ class ObservationView extends StatelessWidget { observation, valueStyle: valueStyle, unitStyle: unitStyle, - locale: locale, componentSeparator: componentSeparator, unknownUnitText: unknownUnitText, unknownValueText: unknownValueText, @@ -37,7 +34,6 @@ class ObservationView extends StatelessWidget { CodeableConceptText(observation.code, style: codeStyle, key: key), _dateTimeWidget = FhirDateTimeText( observation.effectiveDateTime, - locale: locale, style: dateTimeStyle, ); diff --git a/lib/questionnaires/view/item/answer/src/datetime_answer_filler.dart b/lib/questionnaires/view/item/answer/src/datetime_answer_filler.dart index 910fcf61..1b098571 100644 --- a/lib/questionnaires/view/item/answer/src/datetime_answer_filler.dart +++ b/lib/questionnaires/view/item/answer/src/datetime_answer_filler.dart @@ -57,7 +57,6 @@ class _DateTimeInputControl extends AnswerInputControl { return FhirDateTimePicker( focusNode: focusNode, enabled: answerModel.isControlEnabled, - locale: locale, initialDateTime: initialDate, // TODO: This can be specified through minValue / maxValue firstDate: DateTime(1860), From af41121c9ab99f4cf63e244584d0e40798b6a17d Mon Sep 17 00:00:00 2001 From: Hanny Prastya Hariyadi Date: Mon, 2 Oct 2023 19:38:29 +0900 Subject: [PATCH 07/23] Update example --- example/lib/observation_page.dart | 18 ++++++++---- example/lib/primitive_page.dart | 48 +++++++++++++++++++++++-------- 2 files changed, 48 insertions(+), 18 deletions(-) diff --git a/example/lib/observation_page.dart b/example/lib/observation_page.dart index c103a23d..55ffb8cf 100644 --- a/example/lib/observation_page.dart +++ b/example/lib/observation_page.dart @@ -119,12 +119,18 @@ class ObservationPage extends ExhibitPage { const SizedBox( height: 16, ), - ObservationView( - bpObservationWHR, - valueStyle: Theme.of(context).textTheme.headline4, - codeStyle: Theme.of(context).textTheme.subtitle2, - dateTimeStyle: Theme.of(context).textTheme.caption, - locale: const Locale.fromSubtags(languageCode: 'ar', countryCode: 'BH'), + Localizations.override( + context: context, + locale: const Locale.fromSubtags( + languageCode: 'ar', + countryCode: 'BH', + ), + child: ObservationView( + bpObservationWHR, + valueStyle: Theme.of(context).textTheme.headline4, + codeStyle: Theme.of(context).textTheme.subtitle2, + dateTimeStyle: Theme.of(context).textTheme.caption, + ), ), ], ); diff --git a/example/lib/primitive_page.dart b/example/lib/primitive_page.dart index d267357e..dcb1e356 100644 --- a/example/lib/primitive_page.dart +++ b/example/lib/primitive_page.dart @@ -17,25 +17,49 @@ class PrimitivePage extends ExhibitPage { FhirDateTimeText(FhirDateTime('2010-02')), const Spacer(), Text('Germany', style: Theme.of(context).textTheme.headline6), - FhirDateTimeText( - FhirDateTime('2010-02-05 14:02'), - locale: const Locale.fromSubtags(languageCode: 'de', countryCode: 'DE'), + Localizations.override( + context: context, + locale: const Locale.fromSubtags( + languageCode: 'de', + countryCode: 'DE', + ), + child: FhirDateTimeText( + FhirDateTime('2010-02-05 14:02'), + ), ), const Spacer(), Text('Japan', style: Theme.of(context).textTheme.headline6), - FhirDateTimeText( - FhirDateTime('2010-02'), - locale: const Locale.fromSubtags(languageCode: 'ja', countryCode: 'JP'), + Localizations.override( + context: context, + locale: const Locale.fromSubtags( + languageCode: 'ja', + countryCode: 'JP', + ), + child: FhirDateTimeText( + FhirDateTime('2010-02'), + ), ), - FhirDateTimeText( - FhirDateTime('2010-02-05 14:02'), - locale: const Locale.fromSubtags(languageCode: 'ja', countryCode: 'JP'), + Localizations.override( + context: context, + locale: const Locale.fromSubtags( + languageCode: 'ja', + countryCode: 'JP', + ), + child: FhirDateTimeText( + FhirDateTime('2010-02-05 14:02'), + ), ), const Spacer(), Text('Bahrain', style: Theme.of(context).textTheme.headline6), - FhirDateTimeText( - FhirDateTime('2010-02-05 14:02'), - locale: const Locale.fromSubtags(languageCode: 'ar', countryCode: 'BH'), + Localizations.override( + context: context, + locale: const Locale.fromSubtags( + languageCode: 'ar', + countryCode: 'BH', + ), + child: FhirDateTimeText( + FhirDateTime('2010-02-05 14:02'), + ), ), ], ); From 65c0a0526bd5e921e37bfb1c92341caa62a5298a Mon Sep 17 00:00:00 2001 From: Hanny Prastya Hariyadi Date: Tue, 3 Oct 2023 13:36:06 +0900 Subject: [PATCH 08/23] Fix validation not working --- .../custom_questionnaire_stepper_page.dart | 7 ++--- .../model/item/src/response_item_model.dart | 28 +++++++++---------- .../src/questionnaire_response_model.dart | 25 ++++++++--------- .../src/questionnaire_complete_button.dart | 13 +++++---- 4 files changed, 33 insertions(+), 40 deletions(-) diff --git a/example/lib/custom_questionnaire_stepper_page.dart b/example/lib/custom_questionnaire_stepper_page.dart index e18513a8..5487fb97 100644 --- a/example/lib/custom_questionnaire_stepper_page.dart +++ b/example/lib/custom_questionnaire_stepper_page.dart @@ -1,5 +1,4 @@ import 'package:faiadashu/faiadashu.dart'; -import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; import 'package:faiadashu_example/main.dart'; import 'package:flutter/material.dart'; @@ -42,11 +41,9 @@ class _CustomQuestionnaireStepperPageState return; } - try { - matchingItems.first.validate(notifyListeners: true); + final errors = matchingItems.first.validate(notifyListeners: true); + if (errors.isEmpty) { _navigateToNextPage(); - } on ValidationError { - rethrow; } } diff --git a/lib/questionnaires/model/item/src/response_item_model.dart b/lib/questionnaires/model/item/src/response_item_model.dart index 0a7c434d..708f98c4 100644 --- a/lib/questionnaires/model/item/src/response_item_model.dart +++ b/lib/questionnaires/model/item/src/response_item_model.dart @@ -1,6 +1,5 @@ import 'package:faiadashu/faiadashu.dart'; import 'package:faiadashu/questionnaires/model/src/validation_errors/constraint_validation_error.dart'; -import 'package:faiadashu/questionnaires/model/src/validation_errors/custom_validation_error.dart'; import 'package:faiadashu/questionnaires/model/src/validation_errors/required_item_error.dart'; import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; import 'package:fhir/r4.dart'; @@ -60,31 +59,30 @@ abstract class ResponseItemModel extends FillerItemModel { bool updateErrorText = true, bool notifyListeners = false, }) { - if (questionnaireItemModel.isRequired && isUnanswered) { - return [RequiredItemError(nodeUid)]; - } + _exception = questionnaireItemModel.isRequired && isUnanswered + ? RequiredItemError(nodeUid) + : null; + try { validateConstraint(); - _exception = null; } on ValidationError catch (exception) { _exception ??= exception; - if (_exception != exception) { - if (updateErrorText) { - _exception = exception; - } - if (notifyListeners) { - this.notifyListeners(); - } + if (_exception != exception && updateErrorText) { + _exception = exception; } - return [_exception!]; } - return []; + + if (notifyListeners) { + this.notifyListeners(); + } + + return _exception != null ? [_exception!] : []; } /// Returns whether the item is satisfying the `questionnaire-constraint`. /// - /// Throws [CustomValidationError] with human-readable text if not satisfied. + /// Throws [ValidationError] if not satisfied. void validateConstraint() { final constraintExpression = _constraintExpression; if (constraintExpression == null) { diff --git a/lib/questionnaires/model/src/questionnaire_response_model.dart b/lib/questionnaires/model/src/questionnaire_response_model.dart index fb83cc9b..3dbb181b 100644 --- a/lib/questionnaires/model/src/questionnaire_response_model.dart +++ b/lib/questionnaires/model/src/questionnaire_response_model.dart @@ -847,27 +847,24 @@ class QuestionnaireResponseModel { /// * All filled fields are valid /// * All expression-based constraints are satisfied /// - /// Returns null, if everything is complete. - /// Returns a map (UID -> error text) with incomplete entries, if items are incomplete. - Map? validate({ - required FDashLocalizations localizations, + /// Returns an empty list if everything is complete; otherwise, + /// returns a list of [ValidationError], resulting from validation + /// for each [ResponseItemModel.validate]. + List validate({ bool updateErrorText = true, bool notifyListeners = false, }) { - final invalidMap = {}; + List validationErrors = []; for (final itemModel in orderedResponseItemModels()) { - try { - itemModel.validate( - updateErrorText: updateErrorText, - notifyListeners: notifyListeners, - ); - } on ValidationError catch (exception) { - _logger.debug('$itemModel is invalid.'); - } + final errors = itemModel.validate( + updateErrorText: updateErrorText, + notifyListeners: notifyListeners, + ); + validationErrors.addAll(errors); } - return invalidMap.isNotEmpty ? invalidMap : null; + return validationErrors; } /// A map of UIDs -> error texts of invalid [ResponseNode]s. diff --git a/lib/questionnaires/view/src/questionnaire_complete_button.dart b/lib/questionnaires/view/src/questionnaire_complete_button.dart index eaf36c2d..dbd1266b 100644 --- a/lib/questionnaires/view/src/questionnaire_complete_button.dart +++ b/lib/questionnaires/view/src/questionnaire_complete_button.dart @@ -31,15 +31,16 @@ class _QuestionnaireCompleteButtonState final currentResponseStatus = qrm.responseStatus; if (currentResponseStatus != QuestionnaireResponseStatus.completed) { - final incompleteItems = qrm.validate( - localizations: FDashLocalizations.of(context), + final validationErrors = qrm.validate( notifyListeners: true, ); - qrm.invalidityNotifier.value = incompleteItems; + qrm.invalidityNotifier.value = { + for (var validation in validationErrors) + validation.nodeUid: + validation.getMessage(FDashLocalizations.of(context)) ?? "" + }; - if (incompleteItems != null) { - return; - } + return; } final newResponseStatus = From 98b2524af9b986443fe7b93d7033cc3de082e406 Mon Sep 17 00:00:00 2001 From: Hanny Prastya Hariyadi Date: Wed, 4 Oct 2023 17:08:04 +0900 Subject: [PATCH 09/23] Add CommonValidationError --- ...ion_error.dart => common_validation_error.dart} | 4 ++-- .../src/validation_errors/fhir_path_error.dart | 14 -------------- 2 files changed, 2 insertions(+), 16 deletions(-) rename lib/questionnaires/model/src/validation_errors/{custom_validation_error.dart => common_validation_error.dart} (71%) delete mode 100644 lib/questionnaires/model/src/validation_errors/fhir_path_error.dart diff --git a/lib/questionnaires/model/src/validation_errors/custom_validation_error.dart b/lib/questionnaires/model/src/validation_errors/common_validation_error.dart similarity index 71% rename from lib/questionnaires/model/src/validation_errors/custom_validation_error.dart rename to lib/questionnaires/model/src/validation_errors/common_validation_error.dart index 1351d41f..77b3dbab 100644 --- a/lib/questionnaires/model/src/validation_errors/custom_validation_error.dart +++ b/lib/questionnaires/model/src/validation_errors/common_validation_error.dart @@ -1,9 +1,9 @@ import 'package:faiadashu/l10n/src/fdash_localizations.g.dart'; import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; -class CustomValidationError extends ValidationError { +class CommonValidationError extends ValidationError { final String? message; - const CustomValidationError(String nodeUid, this.message) : super(nodeUid); + const CommonValidationError(String nodeUid, this.message) : super(nodeUid); @override String? getMessage(FDashLocalizations localizations) { diff --git a/lib/questionnaires/model/src/validation_errors/fhir_path_error.dart b/lib/questionnaires/model/src/validation_errors/fhir_path_error.dart deleted file mode 100644 index 52a889ab..00000000 --- a/lib/questionnaires/model/src/validation_errors/fhir_path_error.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:faiadashu/l10n/src/fdash_localizations.g.dart'; -import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; -import 'package:fhir_path/fhir_path.dart'; - -class FhirPathError extends ValidationError { - final FhirPathEvaluationException exception; - - const FhirPathError(String nodeUid, this.exception) : super(nodeUid); - - @override - String? getMessage(FDashLocalizations localizations) { - return exception.message; - } -} From 4924422b0d709f09726961a199af981fc367d50a Mon Sep 17 00:00:00 2001 From: Hanny Prastya Hariyadi Date: Wed, 4 Oct 2023 17:14:23 +0900 Subject: [PATCH 10/23] Remove throwing and catching ValidationError --- .../model/item/answer/src/answer_model.dart | 40 ++++++----- .../answer/src/attachment_answer_model.dart | 11 +-- .../item/answer/src/boolean_answer_model.dart | 5 +- .../item/answer/src/coding_answer_model.dart | 15 ++-- .../answer/src/datetime_answer_model.dart | 8 ++- .../answer/src/numerical_answer_model.dart | 20 +++--- .../item/answer/src/string_answer_model.dart | 71 ++++++++++--------- .../answer/src/unsupported_answer_model.dart | 5 +- .../model/item/src/question_item_model.dart | 8 ++- .../model/item/src/response_item_model.dart | 14 ++-- .../answer/src/numerical_answer_filler.dart | 13 ++-- .../item/answer/src/string_answer_filler.dart | 12 +--- lib/questionnaires/view/src/xhtml.dart | 6 +- 13 files changed, 118 insertions(+), 110 deletions(-) diff --git a/lib/questionnaires/model/item/answer/src/answer_model.dart b/lib/questionnaires/model/item/answer/src/answer_model.dart index 8d9198d0..4678c53b 100644 --- a/lib/questionnaires/model/item/answer/src/answer_model.dart +++ b/lib/questionnaires/model/item/answer/src/answer_model.dart @@ -76,14 +76,16 @@ abstract class AnswerModel extends ResponseNode { /// /// This is used to validate external input from a view. /// - /// Throws [ValidationError] when when [inputValue] is invalid. - void validateInput(I? inputValue); + /// Returns null when [inputValue] is invalid; otherwise + /// Returns [ValidationError]. + ValidationError? validateInput(I? inputValue); /// Validates a value against the constraints of the answer model. /// Does not change the [value] of the answer model. /// - /// Throws [ValidationError] when when [inputValue] is invalid. - void validateValue(V? inputValue); + /// Returns null when [inputValue] is invalid; otherwise + /// Returns [ValidationError]. + ValidationError? validateValue(V? inputValue); /// Validates whether the current [value] will pass the completeness check. /// @@ -99,19 +101,21 @@ abstract class AnswerModel extends ResponseNode { bool updateErrorText = true, bool notifyListeners = false, }) { - try { - validateValue(value); - - if (notifyListeners) { - this.notifyListeners(); - } - return []; - } on ValidationError catch (exception) { - if (updateErrorText) { - _error = exception; - } - return [exception]; + final validationError = validateValue(value); + + if (_validationError == validationError && validationError != null) { + return [validationError]; + } + + if (updateErrorText) { + _validationError = validationError; } + + if (notifyListeners) { + this.notifyListeners(); + } + + return _validationError != null ? [_validationError!] : []; } /// Returns whether any answer (valid or invalid) has been provided. @@ -120,13 +124,13 @@ abstract class AnswerModel extends ResponseNode { /// Returns whether this question is unanswered. bool get isEmpty; - ValidationError? _error; + ValidationError? _validationError; /// Returns an error text for display in the answer's control. /// /// This might return an error text from the parent [QuestionItemModel]. String? displayErrorText(FDashLocalizations localizations) => - _error?.getMessage(localizations) ?? + _validationError?.getMessage(localizations) ?? responseItemModel.getErrorText(localizations); /// Returns a [QuestionnaireResponseAnswer] based on the current value. diff --git a/lib/questionnaires/model/item/answer/src/attachment_answer_model.dart b/lib/questionnaires/model/item/answer/src/attachment_answer_model.dart index abdeae1f..5eb32e91 100644 --- a/lib/questionnaires/model/item/answer/src/attachment_answer_model.dart +++ b/lib/questionnaires/model/item/answer/src/attachment_answer_model.dart @@ -2,6 +2,7 @@ import 'package:faiadashu/fhir_types/fhir_types.dart'; import 'package:faiadashu/questionnaires/model/model.dart'; import 'package:faiadashu/questionnaires/model/src/validation_errors/max_size_error.dart'; import 'package:faiadashu/questionnaires/model/src/validation_errors/mime_types_error.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; import 'package:fhir/r4.dart'; class AttachmentAnswerModel extends AnswerModel { @@ -29,18 +30,18 @@ class AttachmentAnswerModel extends AnswerModel { : RenderingString.nullText; @override - void validateInput(Attachment? inValue) { - validateValue(inValue); + ValidationError? validateInput(Attachment? inValue) { + return validateValue(inValue); } @override - void validateValue(Attachment? inputValue) { + ValidationError? validateValue(Attachment? inputValue) { if (inputValue == null) return null; if (maxSize > 0) { final attachmentSize = inputValue.size?.value; if (attachmentSize == null || attachmentSize > maxSize) { - throw MaxSizeError(nodeUid, maxSize); + return MaxSizeError(nodeUid, maxSize); } } @@ -48,7 +49,7 @@ class AttachmentAnswerModel extends AnswerModel { final attachmentMimeType = inputValue.contentType?.value; if (attachmentMimeType == null || !mimeTypes.contains(attachmentMimeType)) { - throw MimeTypesError(nodeUid, mimeTypes); + return MimeTypesError(nodeUid, mimeTypes); } } diff --git a/lib/questionnaires/model/item/answer/src/boolean_answer_model.dart b/lib/questionnaires/model/item/answer/src/boolean_answer_model.dart index f00eaf54..28e633a8 100644 --- a/lib/questionnaires/model/item/answer/src/boolean_answer_model.dart +++ b/lib/questionnaires/model/item/answer/src/boolean_answer_model.dart @@ -1,4 +1,5 @@ import 'package:faiadashu/questionnaires/model/model.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; import 'package:fhir/r4.dart'; class BooleanAnswerModel extends AnswerModel { @@ -23,12 +24,12 @@ class BooleanAnswerModel extends AnswerModel { ); @override - void validateInput(Boolean? inValue) { + ValidationError? validateInput(Boolean? inValue) { return null; } @override - void validateValue(Boolean? inputValue) { + ValidationError? validateValue(Boolean? inputValue) { return null; } diff --git a/lib/questionnaires/model/item/answer/src/coding_answer_model.dart b/lib/questionnaires/model/item/answer/src/coding_answer_model.dart index 775ab28a..c6709511 100644 --- a/lib/questionnaires/model/item/answer/src/coding_answer_model.dart +++ b/lib/questionnaires/model/item/answer/src/coding_answer_model.dart @@ -6,6 +6,7 @@ import 'package:faiadashu/questionnaires/model/model.dart'; import 'package:faiadashu/questionnaires/model/src/validation_errors/max_occurs_error.dart'; import 'package:faiadashu/questionnaires/model/src/validation_errors/min_occurs_error.dart'; import 'package:faiadashu/questionnaires/model/src/validation_errors/single_selection_or_open_string_error.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; import 'package:faiadashu/questionnaires/questionnaires.dart'; import 'package:fhir/r4.dart'; @@ -306,14 +307,14 @@ class CodingAnswerModel extends AnswerModel { } @override - void validateInput(OptionsOrString? inValue) { + ValidationError? validateInput(OptionsOrString? inValue) { return validateValue(inValue); } @override - void validateValue(OptionsOrString? inValue) { + ValidationError? validateValue(OptionsOrString? inValue) { if (inValue == null) { - return; + return null; } final selectedOptionsCount = inValue.selectedOptions?.length ?? 0; @@ -323,18 +324,20 @@ class CodingAnswerModel extends AnswerModel { if (!(questionnaireItemModel.questionnaireItem.repeats?.value ?? false)) { if (totalCount != 1) { - throw SingleSelectionOrOpenStringError(nodeUid, _openLabel); + return SingleSelectionOrOpenStringError(nodeUid, _openLabel); } } if (totalCount < minOccurs) { - throw MinOccursError(nodeUid, minOccurs); + return MinOccursError(nodeUid, minOccurs); } final maxOccurs = this.maxOccurs; if (maxOccurs != null && totalCount > maxOccurs) { - throw MaxOccursError(nodeUid, maxOccurs); + return MaxOccursError(nodeUid, maxOccurs); } + + return null; } @override diff --git a/lib/questionnaires/model/item/answer/src/datetime_answer_model.dart b/lib/questionnaires/model/item/answer/src/datetime_answer_model.dart index 541198bd..711ae212 100644 --- a/lib/questionnaires/model/item/answer/src/datetime_answer_model.dart +++ b/lib/questionnaires/model/item/answer/src/datetime_answer_model.dart @@ -1,6 +1,7 @@ import 'package:faiadashu/fhir_types/fhir_types.dart'; import 'package:faiadashu/questionnaires/model/model.dart'; import 'package:faiadashu/questionnaires/model/src/validation_errors/date_time_error.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; import 'package:fhir/r4.dart' show Date, @@ -53,15 +54,16 @@ class DateTimeAnswerModel extends AnswerModel { } @override - void validateInput(FhirDateTime? inValue) { + ValidationError? validateInput(FhirDateTime? inValue) { return validateValue(inValue); } @override - void validateValue(FhirDateTime? inValue) { + ValidationError? validateValue(FhirDateTime? inValue) { if (!(inValue == null || inValue.isValid)) { - throw DateTimeError(nodeUid); + return DateTimeError(nodeUid); } + return null; } @override diff --git a/lib/questionnaires/model/item/answer/src/numerical_answer_model.dart b/lib/questionnaires/model/item/answer/src/numerical_answer_model.dart index 935a76e7..0b72417d 100644 --- a/lib/questionnaires/model/item/answer/src/numerical_answer_model.dart +++ b/lib/questionnaires/model/item/answer/src/numerical_answer_model.dart @@ -1,11 +1,11 @@ import 'package:faiadashu/coding/coding.dart'; -import 'package:faiadashu/extensions/function_extension.dart'; import 'package:faiadashu/fhir_types/fhir_types.dart'; import 'package:faiadashu/logging/logging.dart'; import 'package:faiadashu/questionnaires/model/model.dart'; import 'package:faiadashu/questionnaires/model/src/validation_errors/max_value_error.dart'; import 'package:faiadashu/questionnaires/model/src/validation_errors/min_value_error.dart'; import 'package:faiadashu/questionnaires/model/src/validation_errors/nan_error.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; import 'package:fhir/r4.dart'; import 'package:intl/intl.dart'; @@ -189,9 +189,9 @@ class NumericalAnswerModel extends AnswerModel { : RenderingString.nullText; @override - void validateInput(String? inputValue) { + ValidationError? validateInput(String? inputValue) { if (inputValue == null || inputValue.isEmpty) { - return; + return null; } num number = double.nan; try { @@ -200,16 +200,16 @@ class NumericalAnswerModel extends AnswerModel { // Ignore FormatException, number remains nan. } if (number.isNaN) { - throw NanError(nodeUid); + return NanError(nodeUid); } final quantity = _valueFromNumber(number); - validateValue(quantity); + return validateValue(quantity); } @override - void validateValue(Quantity? inputValue) { + ValidationError? validateValue(Quantity? inputValue) { if (inputValue == null) { return null; } @@ -221,10 +221,10 @@ class NumericalAnswerModel extends AnswerModel { } if (number > _maxValue) { - throw MaxValueError(nodeUid, Decimal(_maxValue).format(locale)); + return MaxValueError(nodeUid, Decimal(_maxValue).format(locale)); } if (number < _minValue) { - throw MinValueError(nodeUid, Decimal(_minValue).format(locale)); + return MinValueError(nodeUid, Decimal(_minValue).format(locale)); } return null; @@ -265,9 +265,9 @@ class NumericalAnswerModel extends AnswerModel { /// * Updates the numerical value based on text input /// * Keeps the unit Quantity? copyWithTextInput(String textInput) { - final valid = (() => validateInput(textInput)).callSafely() ?? false; + final valid = validateInput(textInput); - final dataAbsentReasonExtension = !valid + final dataAbsentReasonExtension = valid != null ? [ FhirExtension( url: dataAbsentReasonExtensionUrl, diff --git a/lib/questionnaires/model/item/answer/src/string_answer_model.dart b/lib/questionnaires/model/item/answer/src/string_answer_model.dart index eb7077a3..d0b48dfd 100644 --- a/lib/questionnaires/model/item/answer/src/string_answer_model.dart +++ b/lib/questionnaires/model/item/answer/src/string_answer_model.dart @@ -5,6 +5,7 @@ import 'package:faiadashu/questionnaires/model/src/validation_errors/entry_forma import 'package:faiadashu/questionnaires/model/src/validation_errors/min_length_error.dart'; import 'package:faiadashu/questionnaires/model/src/validation_errors/regex_error.dart'; import 'package:faiadashu/questionnaires/model/src/validation_errors/url_error.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; import 'package:fhir/r4.dart'; enum StringAnswerKeyboard { plain, email, phone, number, multiline, url } @@ -64,73 +65,73 @@ class StringAnswerModel extends AnswerModel { : RenderingString.nullText; @override - void validateInput(String? inValue) { + ValidationError? validateInput(String? inValue) { final checkValue = inValue?.trim(); return validateValue(checkValue); } @override - void validateValue(String? inputValue) { + ValidationError? validateValue(String? inputValue) { if (inputValue == null || inputValue.isEmpty) { - return; + return null; } if (inputValue.length < minLength) { - throw MinLengthError(nodeUid, minLength); + return MinLengthError(nodeUid, minLength); } if (maxLength != null && inputValue.length > maxLength!) { - throw MinLengthError(nodeUid, maxLength!); + return MinLengthError(nodeUid, maxLength!); } if (qi.type == QuestionnaireItemType.url) { if (!_urlRegExp.hasMatch(inputValue)) { - throw UrlError(nodeUid); + return UrlError(nodeUid); } } if (regExp != null) { if (!regExp!.hasMatch(inputValue)) { - throw (entryFormat != null) + return (entryFormat != null) ? EntryFormatError(nodeUid, entryFormat!) : RegexError(nodeUid); } } + + return null; } @override QuestionnaireResponseAnswer? createFhirAnswer( List? items, ) { - try { - final value = this.value?.trim(); - - validateInput(value); - - final dataAbsentReasonExtension = [ - FhirExtension( - url: dataAbsentReasonExtensionUrl, - valueCode: dataAbsentReasonAsTextCode, - ), - ]; - - return (value != null && value.isNotEmpty) - ? (qi.type != QuestionnaireItemType.url) - ? QuestionnaireResponseAnswer( - valueString: value, - extension_: dataAbsentReasonExtension, - item: items, - ) - : QuestionnaireResponseAnswer( - valueUri: FhirUri(value), - extension_: dataAbsentReasonExtension, - item: items, - ) - : null; - } on Exception { - return null; - } + final value = this.value?.trim(); + + final valid = validateInput(value) == null; + + final dataAbsentReasonExtension = !valid + ? [ + FhirExtension( + url: dataAbsentReasonExtensionUrl, + valueCode: dataAbsentReasonAsTextCode, + ), + ] + : null; + + return (value != null && value.isNotEmpty) + ? (qi.type != QuestionnaireItemType.url) + ? QuestionnaireResponseAnswer( + valueString: value, + extension_: dataAbsentReasonExtension, + item: items, + ) + : QuestionnaireResponseAnswer( + valueUri: FhirUri(value), + extension_: dataAbsentReasonExtension, + item: items, + ) + : null; } @override diff --git a/lib/questionnaires/model/item/answer/src/unsupported_answer_model.dart b/lib/questionnaires/model/item/answer/src/unsupported_answer_model.dart index f37c9e60..2b5a0c08 100644 --- a/lib/questionnaires/model/item/answer/src/unsupported_answer_model.dart +++ b/lib/questionnaires/model/item/answer/src/unsupported_answer_model.dart @@ -1,4 +1,5 @@ import 'package:faiadashu/questionnaires/model/model.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; import 'package:fhir/r4.dart'; /// A pseudo-model for a questionnaire item of an unsupported type. @@ -16,12 +17,12 @@ class UnsupportedAnswerModel extends AnswerModel { RenderingString get display => RenderingString.nullText; @override - void validateInput(Object? inValue) { + ValidationError? validateInput(Object? inValue) { return null; } @override - void validateValue(Object? inputValue) { + ValidationError? validateValue(Object? inputValue) { return null; } diff --git a/lib/questionnaires/model/item/src/question_item_model.dart b/lib/questionnaires/model/item/src/question_item_model.dart index 0f66ed02..163168b1 100644 --- a/lib/questionnaires/model/item/src/question_item_model.dart +++ b/lib/questionnaires/model/item/src/question_item_model.dart @@ -2,6 +2,7 @@ import 'dart:developer'; import 'package:collection/collection.dart'; import 'package:faiadashu/faiadashu.dart'; +import 'package:faiadashu/questionnaires/model/src/validation_errors/common_validation_error.dart'; import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; import 'package:fhir/primitive_types/primitive_types.dart'; import 'package:fhir/r4/r4.dart'; @@ -419,9 +420,10 @@ class QuestionItemModel extends ResponseItemModel { // Write the value back to the answer model firstAnswerModel.populateFromExpression(evaluationResult); } catch (ex) { - // errorText = - // (ex is FhirPathEvaluationException) ? ex.message : ex.toString(); - if (ex is FhirPathEvaluationException) {} + validationError = CommonValidationError( + nodeUid, + ex is FhirPathEvaluationException ? ex.message : ex.toString(), + ); _qimLogger.warn('Calculation problem: $_calculatedExpression', error: ex); notifyListeners(); // This could be added to a setter for errorText, but might have side-effects. } diff --git a/lib/questionnaires/model/item/src/response_item_model.dart b/lib/questionnaires/model/item/src/response_item_model.dart index 708f98c4..ade3abc3 100644 --- a/lib/questionnaires/model/item/src/response_item_model.dart +++ b/lib/questionnaires/model/item/src/response_item_model.dart @@ -50,26 +50,26 @@ abstract class ResponseItemModel extends FillerItemModel { /// /// Localized text if an error exists. Or null if no error exists. String? getErrorText(FDashLocalizations localizations) { - return _exception?.getMessage(localizations); + return validationError?.getMessage(localizations); } - ValidationError? _exception; + ValidationError? validationError; List validate({ bool updateErrorText = true, bool notifyListeners = false, }) { - _exception = questionnaireItemModel.isRequired && isUnanswered + validationError = questionnaireItemModel.isRequired && isUnanswered ? RequiredItemError(nodeUid) : null; try { validateConstraint(); } on ValidationError catch (exception) { - _exception ??= exception; + validationError ??= exception; - if (_exception != exception && updateErrorText) { - _exception = exception; + if (exception != exception && updateErrorText) { + validationError = exception; } } @@ -77,7 +77,7 @@ abstract class ResponseItemModel extends FillerItemModel { this.notifyListeners(); } - return _exception != null ? [_exception!] : []; + return validationError != null ? [validationError!] : []; } /// Returns whether the item is satisfying the `questionnaire-constraint`. diff --git a/lib/questionnaires/view/item/answer/src/numerical_answer_filler.dart b/lib/questionnaires/view/item/answer/src/numerical_answer_filler.dart index 6ae3adff..6dd98aa1 100644 --- a/lib/questionnaires/view/item/answer/src/numerical_answer_filler.dart +++ b/lib/questionnaires/view/item/answer/src/numerical_answer_filler.dart @@ -1,6 +1,5 @@ import 'package:faiadashu/fhir_types/fhir_types.dart'; import 'package:faiadashu/l10n/l10n.dart'; -import 'package:faiadashu/questionnaires/model/src/validation_errors/custom_validation_error.dart'; import 'package:faiadashu/questionnaires/questionnaires.dart'; import 'package:fhir/r4.dart'; import 'package:flutter/material.dart'; @@ -228,14 +227,12 @@ class _NumberFieldInputControl decimal: answerModel.maxDecimal > 0, ), validator: (inputValue) { - try { - if (itemModel.isCalculated) { - answerModel.validateInput(inputValue); - } - } on CustomValidationError catch (exception) { - return exception.getMessage(FDashLocalizations.of(context)); + if (!itemModel.isCalculated) { + return null; } - return null; + return answerModel + .validateInput(inputValue) + ?.getMessage(FDashLocalizations.of(context)); }, autovalidateMode: (itemModel.isCalculated) ? AutovalidateMode.disabled diff --git a/lib/questionnaires/view/item/answer/src/string_answer_filler.dart b/lib/questionnaires/view/item/answer/src/string_answer_filler.dart index e34cb73e..a88b958f 100644 --- a/lib/questionnaires/view/item/answer/src/string_answer_filler.dart +++ b/lib/questionnaires/view/item/answer/src/string_answer_filler.dart @@ -1,5 +1,4 @@ import 'package:faiadashu/l10n/l10n.dart'; -import 'package:faiadashu/questionnaires/model/src/validation_errors/custom_validation_error.dart'; import 'package:faiadashu/questionnaires/questionnaires.dart'; import 'package:fhir/r4.dart'; import 'package:flutter/material.dart'; @@ -115,14 +114,9 @@ class _StringAnswerInputControl extends AnswerInputControl { ) : null, ), - validator: (inputValue) { - try { - answerModel.validateInput(inputValue); - } on CustomValidationError catch (exception) { - return exception.getMessage(FDashLocalizations.of(context)); - } - return null; - }, + validator: (inputValue) => answerModel + .validateInput(inputValue) + ?.getMessage(FDashLocalizations.of(context)), autovalidateMode: AutovalidateMode.always, onChanged: (content) { answerModel.value = content; diff --git a/lib/questionnaires/view/src/xhtml.dart b/lib/questionnaires/view/src/xhtml.dart index 9ebd120f..c3b46d83 100644 --- a/lib/questionnaires/view/src/xhtml.dart +++ b/lib/questionnaires/view/src/xhtml.dart @@ -29,8 +29,10 @@ class Xhtml extends StatelessWidget { TextStyle? defaultTextStyle, Key? key, }) { - final xhtmlString = - RenderingString.fromText(plainText, extensions: extensions); + final xhtmlString = RenderingString.fromText( + plainText, + extensions: extensions, + ); return Xhtml.fromRenderingString( context, From c98aee0b2e108635a4f225fec9066132c3f0d175 Mon Sep 17 00:00:00 2001 From: Hanny Prastya Hariyadi Date: Wed, 4 Oct 2023 17:15:24 +0900 Subject: [PATCH 11/23] Remove unintended changes --- example/lib/main.dart | 2 +- example/lib/questionnaire_launch_tile.dart | 11 ++++++----- .../item/answer/src/coding_answer_filler.dart | 18 +++++++----------- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index a37fd3cf..29851e01 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -401,7 +401,7 @@ class _HomePageState extends State { child: CustomQuestionnaireStepperPage( fhirResourceProvider: AssetResourceProvider.singleton( questionnaireResourceUri, - 'assets/instruments/triage.json', + 'assets/instruments/framingham-hcdc.json', ), launchContext: launchContext, ), diff --git a/example/lib/questionnaire_launch_tile.dart b/example/lib/questionnaire_launch_tile.dart index cbfe0cec..a296285a 100644 --- a/example/lib/questionnaire_launch_tile.dart +++ b/example/lib/questionnaire_launch_tile.dart @@ -59,12 +59,13 @@ class _QuestionnaireLaunchTileState extends State { } Future _createModelFuture( - FDashLocalizations localizations) { + BuildContext context, + ) { return QuestionnaireResponseModel.fromFhirResourceBundle( - localizations: localizations, + localizations: FDashLocalizations.of(context), fhirResourceProvider: _questionnaireProvider, launchContext: widget.launchContext, - locale: Locale('ja'), + locale: Localizations.localeOf(context), ).then((qrm) { qrm.populate( widget.restoreResponseFunction.call(widget.questionnairePath), @@ -79,7 +80,7 @@ class _QuestionnaireLaunchTileState extends State { _percentPattern = NumberFormat.percentPattern( Localizations.localeOf(context).toString(), ); - _modelFuture = _createModelFuture(FDashLocalizations.of(context)); + _modelFuture = _createModelFuture(context); } @override @@ -238,7 +239,7 @@ class _QuestionnaireLaunchTileState extends State { ).then((value) { // This triggers after return from questionnaire filler setState(() { - _modelFuture = _createModelFuture(FDashLocalizations.of(context)); + _modelFuture = _createModelFuture(context); }); }); }, diff --git a/lib/questionnaires/view/item/answer/src/coding_answer_filler.dart b/lib/questionnaires/view/item/answer/src/coding_answer_filler.dart index eb55ff52..0490f22d 100644 --- a/lib/questionnaires/view/item/answer/src/coding_answer_filler.dart +++ b/lib/questionnaires/view/item/answer/src/coding_answer_filler.dart @@ -46,17 +46,13 @@ class _CodingInputControl extends AnswerInputControl { final errorText = answerModel.displayErrorText(FDashLocalizations.of(context)); - return Column( - children: [ - QuestionnaireTheme.of(context).codingControlLayoutBuilder( - context, - _buildCodingControl(context), - openStringInputControlWidget: answerModel.isOptionsOrString - ? _OpenStringInputControl(answerModel) - : null, - errorText: errorText, - ) - ], + return QuestionnaireTheme.of(context).codingControlLayoutBuilder( + context, + _buildCodingControl(context), + openStringInputControlWidget: answerModel.isOptionsOrString + ? _OpenStringInputControl(answerModel) + : null, + errorText: errorText, ); } From 5def257e223d04d936a9611cace5473f74c63f33 Mon Sep 17 00:00:00 2001 From: Hanny Prastya Hariyadi Date: Wed, 4 Oct 2023 20:37:43 +0900 Subject: [PATCH 12/23] Remove `callSafely` --- lib/extensions/function_extension.dart | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 lib/extensions/function_extension.dart diff --git a/lib/extensions/function_extension.dart b/lib/extensions/function_extension.dart deleted file mode 100644 index 656c0428..00000000 --- a/lib/extensions/function_extension.dart +++ /dev/null @@ -1,26 +0,0 @@ -/// An extension on [Function] that provides a method to call functions -/// safely, which means if the function throws an error, it will catch that -/// error and return `null`, instead of letting the error propagate. -/// -/// This can be useful when working with functions that may throw exceptions -/// but you don't want to handle those exceptions explicitly or when you -/// prefer to deal with a `null` return value in the case of an error. -extension TryOrNull on Function { - /// Calls the function safely and returns the result of the function if it's - /// successful, or `null` if an error occurs. - /// - /// Here, `T` represents the expected return type of the function. - /// - /// Example: - /// ```dart - /// Function riskyFunction = () => throw Exception('Oops!'); - /// final result = riskyFunction.callSafely(); // result will be `null`. - /// ``` - T? callSafely() { - try { - return this.call() as T; - } catch (e) { - return null; - } - } -} From f660a742e870b9417cc668dc7db0dbd5cb9f57fb Mon Sep 17 00:00:00 2001 From: Hanny Prastya Hariyadi Date: Wed, 4 Oct 2023 21:25:49 +0900 Subject: [PATCH 13/23] Prevent xHtmlText from being replaced by tanslation extension --- lib/questionnaires/view/src/xhtml.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/questionnaires/view/src/xhtml.dart b/lib/questionnaires/view/src/xhtml.dart index c3b46d83..c882f157 100644 --- a/lib/questionnaires/view/src/xhtml.dart +++ b/lib/questionnaires/view/src/xhtml.dart @@ -57,10 +57,7 @@ class Xhtml extends StatelessWidget { }) { _logger.trace('enter fromRenderingString $renderingString'); - final xhtml = renderingString.xhtmlText.translate( - renderingString.extensions, - Localizations.localeOf(context), - ); + final xhtml = renderingString.xhtmlText; final plainText = renderingString.plainText.translate( renderingString.extensions, Localizations.localeOf(context), From 2c54bad5d857cd29d599ce00ec037e30aea4ab02 Mon Sep 17 00:00:00 2001 From: Hanny Date: Thu, 5 Oct 2023 18:17:55 +0900 Subject: [PATCH 14/23] Update lib/observations/src/observation_value_view.dart Co-authored-by: fjwong --- lib/observations/src/observation_value_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/observations/src/observation_value_view.dart b/lib/observations/src/observation_value_view.dart index 0fc71666..c40679f6 100644 --- a/lib/observations/src/observation_value_view.dart +++ b/lib/observations/src/observation_value_view.dart @@ -31,7 +31,7 @@ class ObservationValueView extends StatelessWidget { final Widget valueWidget; final decimalFormat = NumberFormat.decimalPattern( - (Localizations.localeOf(context)).toString(), + Localizations.localeOf(context).toString(), ); if (_observation.valueQuantity != null) { From 0962199475ed76c89a402f3516ddce83471e7980 Mon Sep 17 00:00:00 2001 From: Hanny Prastya Hariyadi Date: Wed, 11 Oct 2023 07:55:55 +0900 Subject: [PATCH 15/23] Refactor validateConstraint to return ValidationError? --- .../model/item/src/response_item_model.dart | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/lib/questionnaires/model/item/src/response_item_model.dart b/lib/questionnaires/model/item/src/response_item_model.dart index ade3abc3..b069b2b7 100644 --- a/lib/questionnaires/model/item/src/response_item_model.dart +++ b/lib/questionnaires/model/item/src/response_item_model.dart @@ -59,34 +59,34 @@ abstract class ResponseItemModel extends FillerItemModel { bool updateErrorText = true, bool notifyListeners = false, }) { - validationError = questionnaireItemModel.isRequired && isUnanswered - ? RequiredItemError(nodeUid) - : null; - - try { - validateConstraint(); - } on ValidationError catch (exception) { - validationError ??= exception; + ValidationError? newValidationError; - if (exception != exception && updateErrorText) { - validationError = exception; - } + if (questionnaireItemModel.isRequired && isUnanswered) { + newValidationError = RequiredItemError(nodeUid); } - if (notifyListeners) { - this.notifyListeners(); + final constraintError = validateConstraint(); + newValidationError ??= constraintError; + + if (validationError != newValidationError) { + if (updateErrorText) { + validationError = newValidationError; + } + if (notifyListeners) { + this.notifyListeners(); + } } - return validationError != null ? [validationError!] : []; + return newValidationError != null ? [newValidationError] : []; } /// Returns whether the item is satisfying the `questionnaire-constraint`. /// - /// Throws [ValidationError] if not satisfied. - void validateConstraint() { + /// Returns [ValidationError] if not satisfied; otherwise null + ValidationError? validateConstraint() { final constraintExpression = _constraintExpression; if (constraintExpression == null) { - return; + return null; } final isSatisfied = constraintExpression.fetchBoolValue( @@ -95,11 +95,11 @@ abstract class ResponseItemModel extends FillerItemModel { location: nodeUid, ); - if (!isSatisfied) { - throw ConstraintValidationError( - nodeUid, - questionnaireItemModel.constraintHuman, - ); - } + return isSatisfied + ? null + : ConstraintValidationError( + nodeUid, + questionnaireItemModel.constraintHuman, + ); } } From 47debb7ee4dbaf646adac98ebf9e8ec26256cf75 Mon Sep 17 00:00:00 2001 From: Hanny Prastya Hariyadi Date: Wed, 11 Oct 2023 07:56:58 +0900 Subject: [PATCH 16/23] Fix calculation not triggered --- .../answer/src/numerical_answer_filler.dart | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/lib/questionnaires/view/item/answer/src/numerical_answer_filler.dart b/lib/questionnaires/view/item/answer/src/numerical_answer_filler.dart index 6dd98aa1..457ec33a 100644 --- a/lib/questionnaires/view/item/answer/src/numerical_answer_filler.dart +++ b/lib/questionnaires/view/item/answer/src/numerical_answer_filler.dart @@ -185,6 +185,8 @@ class _NumberFieldInputControl ); } + final localizations = FDashLocalizations.of(context); + return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -196,8 +198,7 @@ class _NumberFieldInputControl textAlignVertical: TextAlignVertical.center, textAlign: TextAlign.end, decoration: InputDecoration( - errorText: - answerModel.displayErrorText(FDashLocalizations.of(context)), + errorText: answerModel.displayErrorText(localizations), errorStyle: (itemModel .isCalculated) // Force display of error text on calculated item ? TextStyle( @@ -208,11 +209,10 @@ class _NumberFieldInputControl prefixIcon: itemModel.isCalculated ? Icon( Icons.calculate, - color: (answerModel.displayErrorText( - FDashLocalizations.of(context)) != - null) - ? Theme.of(context).errorColor - : null, + color: + (answerModel.displayErrorText(localizations) != null) + ? Theme.of(context).errorColor + : null, ) : null, suffixIcon: (answerModel.hasUnitChoices) @@ -226,14 +226,13 @@ class _NumberFieldInputControl signed: answerModel.minValue < 0, decimal: answerModel.maxDecimal > 0, ), - validator: (inputValue) { - if (!itemModel.isCalculated) { - return null; - } - return answerModel - .validateInput(inputValue) - ?.getMessage(FDashLocalizations.of(context)); - }, + validator: (itemModel.isCalculated) + ? null + : (inputValue) { + return answerModel + .validateInput(inputValue) + ?.getMessage(localizations); + }, autovalidateMode: (itemModel.isCalculated) ? AutovalidateMode.disabled : AutovalidateMode.always, From 9bbea4e66ffdea6082a831bfd31c47102a2a606a Mon Sep 17 00:00:00 2001 From: Hanny Prastya Hariyadi Date: Wed, 11 Oct 2023 12:34:50 +0900 Subject: [PATCH 17/23] Improve localization by doing translation in RenderingString --- .../item/answer/src/coding_answer_option_model.dart | 10 +++++++++- .../model/src/questionnaire_item_model.dart | 2 ++ lib/questionnaires/model/src/questionnaire_model.dart | 1 + lib/questionnaires/model/src/rendering_string.dart | 8 ++++++-- .../view/item/src/questionnaire_item_filler_title.dart | 6 +----- lib/questionnaires/view/src/questionnaire_filler.dart | 3 +++ lib/questionnaires/view/src/xhtml.dart | 7 ++----- 7 files changed, 24 insertions(+), 13 deletions(-) diff --git a/lib/questionnaires/model/item/answer/src/coding_answer_option_model.dart b/lib/questionnaires/model/item/answer/src/coding_answer_option_model.dart index 20b41ea0..7c37bccd 100644 --- a/lib/questionnaires/model/item/answer/src/coding_answer_option_model.dart +++ b/lib/questionnaires/model/item/answer/src/coding_answer_option_model.dart @@ -75,6 +75,7 @@ class CodingAnswerOptionModel { final plainText = coding.localizedDisplay(locale); optionText = RenderingString.fromText( plainText, + locale: locale, extensions: coding.displayElement?.extension_, ); forDisplay = plainText; @@ -132,7 +133,11 @@ class CodingAnswerOptionModel { ?.valueString; final optionPrefix = (plainOptionPrefix != null) - ? RenderingString.fromText(plainOptionPrefix, extensions: extensions) + ? RenderingString.fromText( + plainOptionPrefix, + locale: locale, + extensions: extensions, + ) : null; RenderingString optionText; @@ -146,6 +151,7 @@ class CodingAnswerOptionModel { forDisplay = plainText; optionText = RenderingString.fromText( plainText, + locale: locale, extensions: xhtmlExtensions, ); } else { @@ -154,6 +160,7 @@ class CodingAnswerOptionModel { forDisplay = _createForDisplay(coding, locale, questionnaireItemModel); optionText = RenderingString.fromText( plainText, + locale: locale, extensions: extensions, ); } @@ -170,6 +177,7 @@ class CodingAnswerOptionModel { forDisplay = plainText; optionText = RenderingString.fromText( plainText, + locale: locale, extensions: xhtmlExtensions, ); } diff --git a/lib/questionnaires/model/src/questionnaire_item_model.dart b/lib/questionnaires/model/src/questionnaire_item_model.dart index cf4b105c..a7e07567 100644 --- a/lib/questionnaires/model/src/questionnaire_item_model.dart +++ b/lib/questionnaires/model/src/questionnaire_item_model.dart @@ -273,6 +273,7 @@ class QuestionnaireItemModel with Diagnosticable { return (plainText != null) ? RenderingString.fromText( plainText, + locale: questionnaireModel.locale, extensions: questionnaireItem.textElement?.extension_, ) : null; @@ -290,6 +291,7 @@ class QuestionnaireItemModel with Diagnosticable { return (plainPrefix != null) ? RenderingString.fromText( plainPrefix, + locale: questionnaireModel.locale, extensions: questionnaireItem.prefixElement?.extension_, ) : null; diff --git a/lib/questionnaires/model/src/questionnaire_model.dart b/lib/questionnaires/model/src/questionnaire_model.dart index a4d470ea..7a401e02 100644 --- a/lib/questionnaires/model/src/questionnaire_model.dart +++ b/lib/questionnaires/model/src/questionnaire_model.dart @@ -113,6 +113,7 @@ class QuestionnaireModel { return (plainTitle != null) ? RenderingString.fromText( plainTitle, + locale: locale, extensions: questionnaire.titleElement?.extension_, ) : null; diff --git a/lib/questionnaires/model/src/rendering_string.dart b/lib/questionnaires/model/src/rendering_string.dart index 20607959..c0494f30 100644 --- a/lib/questionnaires/model/src/rendering_string.dart +++ b/lib/questionnaires/model/src/rendering_string.dart @@ -1,8 +1,10 @@ import 'dart:convert'; +import 'package:faiadashu/extensions/string_extension.dart'; import 'package:faiadashu/fhir_types/fhir_types.dart'; import 'package:fhir/r4.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:markdown/markdown.dart'; /// Representation of a Fhir string which is plain text with optional @@ -62,6 +64,7 @@ class RenderingString with Diagnosticable { /// * rendering-xhtml factory RenderingString.fromText( String plainText, { + Locale? locale, List? extensions, String? xhtmlText, }) { @@ -83,7 +86,8 @@ class RenderingString with Diagnosticable { ) ?.valueMarkdown; - final escapedPlainText = _htmlEscape.convert(plainText); + final text = plainText.translate(extensions, locale); + final escapedPlainText = _htmlEscape.convert(text); final outputXhtmlText = (xhtmlText != null) ? xhtmlText @@ -98,7 +102,7 @@ class RenderingString with Diagnosticable { ) : (renderingStyle != null) ? '$escapedPlainText' - : plainText; + : text; return RenderingString._( plainText, diff --git a/lib/questionnaires/view/item/src/questionnaire_item_filler_title.dart b/lib/questionnaires/view/item/src/questionnaire_item_filler_title.dart index 95057a6b..89904ca2 100644 --- a/lib/questionnaires/view/item/src/questionnaire_item_filler_title.dart +++ b/lib/questionnaires/view/item/src/questionnaire_item_filler_title.dart @@ -1,4 +1,3 @@ -import 'package:faiadashu/extensions/string_extension.dart'; import 'package:faiadashu/fhir_types/fhir_types.dart'; import 'package:faiadashu/logging/logging.dart'; import 'package:faiadashu/questionnaires/questionnaires.dart'; @@ -50,10 +49,7 @@ class QuestionnaireItemFillerTitle extends StatelessWidget { : '

'; final prefixText = fillerItem.prefix; - final title = text.xhtmlText.translate( - questionnaireItemModel.text?.extensions ?? [], - Localizations.localeOf(context), - ); + final title = text.xhtmlText; final htmlTitleText = (prefixText != null) ? '$openStyleTag${prefixText.xhtmlText} ${title}$requiredTag$closeStyleTag' diff --git a/lib/questionnaires/view/src/questionnaire_filler.dart b/lib/questionnaires/view/src/questionnaire_filler.dart index 3a0faca4..163a76ae 100644 --- a/lib/questionnaires/view/src/questionnaire_filler.dart +++ b/lib/questionnaires/view/src/questionnaire_filler.dart @@ -144,6 +144,9 @@ class _QuestionnaireResponseFillerState return QuestionnaireLoadingIndicator(snapshot); } + if (_questionnaireResponseModel?.locale != snapshot.data?.locale) { + _handleQuestionnaireResponseModelChangeListenerFunction = null; + } if (snapshot.hasData) { _logger.debug('FutureBuilder hasData'); _questionnaireResponseModel = snapshot.data; diff --git a/lib/questionnaires/view/src/xhtml.dart b/lib/questionnaires/view/src/xhtml.dart index c882f157..382534cf 100644 --- a/lib/questionnaires/view/src/xhtml.dart +++ b/lib/questionnaires/view/src/xhtml.dart @@ -1,4 +1,3 @@ -import 'package:faiadashu/extensions/string_extension.dart'; import 'package:faiadashu/fhir_types/fhir_types.dart'; import 'package:faiadashu/logging/logging.dart'; import 'package:faiadashu/questionnaires/model/model.dart'; @@ -31,6 +30,7 @@ class Xhtml extends StatelessWidget { }) { final xhtmlString = RenderingString.fromText( plainText, + locale: Localizations.localeOf(context), extensions: extensions, ); @@ -58,10 +58,7 @@ class Xhtml extends StatelessWidget { _logger.trace('enter fromRenderingString $renderingString'); final xhtml = renderingString.xhtmlText; - final plainText = renderingString.plainText.translate( - renderingString.extensions, - Localizations.localeOf(context), - ); + final plainText = renderingString.plainText; if (renderingString.isPlain) { return Xhtml._( From e9bb381294b3621f403e3bbfb8af7210db3ed8d0 Mon Sep 17 00:00:00 2001 From: Hanny Prastya Hariyadi Date: Wed, 11 Oct 2023 12:49:59 +0900 Subject: [PATCH 18/23] Revert changes and remove unneeded parameters --- .../item/answer/src/string_answer_filler.dart | 12 +++++------- .../view/item/src/questionnaire_item_filler.dart | 11 ++++++----- .../item/src/questionnaire_item_filler_title.dart | 1 - .../view/src/questionnaire_complete_button.dart | 4 +++- .../view/src/questionnaire_filler.dart | 8 +++----- .../view/src/questionnaire_stepper_page_view.dart | 3 --- .../view/src/questionnaire_theme.dart | 15 +++++++-------- 7 files changed, 24 insertions(+), 30 deletions(-) diff --git a/lib/questionnaires/view/item/answer/src/string_answer_filler.dart b/lib/questionnaires/view/item/answer/src/string_answer_filler.dart index a88b958f..7e072838 100644 --- a/lib/questionnaires/view/item/answer/src/string_answer_filler.dart +++ b/lib/questionnaires/view/item/answer/src/string_answer_filler.dart @@ -61,6 +61,7 @@ class _StringAnswerInputControl extends AnswerInputControl { @override Widget build(BuildContext context) { final answerModel = this.answerModel; + final locale = FDashLocalizations.of(context); // FIXME: What should be the repaint mechanism for calculated items? // (it is getting repainted currently, but further optimization might break that) @@ -95,7 +96,7 @@ class _StringAnswerInputControl extends AnswerInputControl { ? QuestionnaireTheme.of(context).maxLinesForTextItem : 1, decoration: InputDecoration( - errorText: answerModel.displayErrorText(FDashLocalizations.of(context)), + errorText: answerModel.displayErrorText(locale), errorStyle: (itemModel .isCalculated) // Force display of error text on calculated item ? TextStyle( @@ -106,17 +107,14 @@ class _StringAnswerInputControl extends AnswerInputControl { prefixIcon: itemModel.isCalculated ? Icon( Icons.calculate, - color: (answerModel - .displayErrorText(FDashLocalizations.of(context)) != - null) + color: (answerModel.displayErrorText(locale) != null) ? Theme.of(context).errorColor : null, ) : null, ), - validator: (inputValue) => answerModel - .validateInput(inputValue) - ?.getMessage(FDashLocalizations.of(context)), + validator: (inputValue) => + answerModel.validateInput(inputValue)?.getMessage(locale), autovalidateMode: AutovalidateMode.always, onChanged: (content) { answerModel.value = content; diff --git a/lib/questionnaires/view/item/src/questionnaire_item_filler.dart b/lib/questionnaires/view/item/src/questionnaire_item_filler.dart index c595e392..9b6a6a49 100644 --- a/lib/questionnaires/view/item/src/questionnaire_item_filler.dart +++ b/lib/questionnaires/view/item/src/questionnaire_item_filler.dart @@ -17,11 +17,7 @@ abstract class QuestionnaireItemFiller extends StatefulWidget { abstract class QuestionnaireItemFillerState extends State { - late final Widget? _titleWidget = QuestionnaireItemFillerTitle.fromFillerItem( - context: context, - fillerItem: widget.fillerItemModel, - questionnaireTheme: questionnaireTheme, - ); + late final Widget? _titleWidget; Widget? get titleWidget => _titleWidget; QuestionnaireFillerData? _questionnaireFiller; @@ -36,6 +32,11 @@ abstract class QuestionnaireItemFillerState void initState() { super.initState(); _focusNode = FocusNode(debugLabel: responseUid, skipTraversal: true); + + _titleWidget = QuestionnaireItemFillerTitle.fromFillerItem( + fillerItem: widget.fillerItemModel, + questionnaireTheme: questionnaireTheme, + ); } @override diff --git a/lib/questionnaires/view/item/src/questionnaire_item_filler_title.dart b/lib/questionnaires/view/item/src/questionnaire_item_filler_title.dart index 89904ca2..c0f4f76f 100644 --- a/lib/questionnaires/view/item/src/questionnaire_item_filler_title.dart +++ b/lib/questionnaires/view/item/src/questionnaire_item_filler_title.dart @@ -19,7 +19,6 @@ class QuestionnaireItemFillerTitle extends StatelessWidget { }) : super(key: key); static Widget? fromFillerItem({ - required BuildContext context, required FillerItemModel fillerItem, required QuestionnaireThemeData questionnaireTheme, Key? key, diff --git a/lib/questionnaires/view/src/questionnaire_complete_button.dart b/lib/questionnaires/view/src/questionnaire_complete_button.dart index dbd1266b..665106b2 100644 --- a/lib/questionnaires/view/src/questionnaire_complete_button.dart +++ b/lib/questionnaires/view/src/questionnaire_complete_button.dart @@ -40,7 +40,9 @@ class _QuestionnaireCompleteButtonState validation.getMessage(FDashLocalizations.of(context)) ?? "" }; - return; + if (validationErrors.isNotEmpty) { + return; + } } final newResponseStatus = diff --git a/lib/questionnaires/view/src/questionnaire_filler.dart b/lib/questionnaires/view/src/questionnaire_filler.dart index 163a76ae..78f28ad8 100644 --- a/lib/questionnaires/view/src/questionnaire_filler.dart +++ b/lib/questionnaires/view/src/questionnaire_filler.dart @@ -250,7 +250,7 @@ class QuestionnaireFillerData extends InheritedWidget { /// The [QuestionnaireItemFiller]s are ordered based on 'pre-order'. /// /// see: https://en.wikipedia.org/wiki/Tree_traversal#Pre-order,_NLR - QuestionnaireItemFiller itemFillerAt(BuildContext context, int index) { + QuestionnaireItemFiller itemFillerAt(int index) { _logger.trace('itemFillerAt $index'); final item = _itemFillers[index]; @@ -259,7 +259,6 @@ class QuestionnaireFillerData extends InheritedWidget { _itemFillers[index] = questionnaireTheme.createQuestionnaireItemFiller( this, fillerItemModels.elementAt(index), - Localizations.localeOf(context), key: ValueKey( 'item-filler-${fillerItemModels.elementAt(index).nodeUid}', ), @@ -289,11 +288,10 @@ class QuestionnaireFillerData extends InheritedWidget { /// Returns the [QuestionnaireItemFiller] of the [visibleIndex]-th item that is currently /// visible. - QuestionnaireItemFiller? visibleItemFillerAt( - BuildContext context, int visibleIndex) { + QuestionnaireItemFiller? visibleItemFillerAt(int visibleIndex) { final index = indexOfVisibleItemAt(visibleIndex); - return index < 0 ? null : itemFillerAt(context, index); + return index < 0 ? null : itemFillerAt(index); } /// Returns the integer range of items corresponding to the [visibleRootIndex]-th root item diff --git a/lib/questionnaires/view/src/questionnaire_stepper_page_view.dart b/lib/questionnaires/view/src/questionnaire_stepper_page_view.dart index b9d90139..999b664b 100644 --- a/lib/questionnaires/view/src/questionnaire_stepper_page_view.dart +++ b/lib/questionnaires/view/src/questionnaire_stepper_page_view.dart @@ -50,7 +50,6 @@ class _QuestionnaireStepperPageViewState final fillerData = QuestionnaireResponseFiller.of(context); final nextPageFillerItem = themeData.stepperQuestionnaireItemFiller( - context, fillerData, currentPage + 1, ); @@ -77,7 +76,6 @@ class _QuestionnaireStepperPageViewState final responseFiller = QuestionnaireResponseFiller.of(context); final data = QuestionnaireTheme.of(context).stepperQuestionnaireItemFiller( - context, responseFiller, index, ); @@ -107,7 +105,6 @@ class _QuestionnaireStepperPageViewState final data = QuestionnaireTheme.of(context).stepperQuestionnaireItemFiller( - context, responseFillerData, index, ); diff --git a/lib/questionnaires/view/src/questionnaire_theme.dart b/lib/questionnaires/view/src/questionnaire_theme.dart index 9ca28a81..9047d299 100644 --- a/lib/questionnaires/view/src/questionnaire_theme.dart +++ b/lib/questionnaires/view/src/questionnaire_theme.dart @@ -151,7 +151,6 @@ class QuestionnaireThemeData { /// /// [pageIndex] is the index of the page that's being currently built. final QuestionnaireItemFiller? Function( - BuildContext context, QuestionnaireFillerData responseFiller, int pageIndex, ) stepperQuestionnaireItemFiller; @@ -177,12 +176,14 @@ class QuestionnaireThemeData { this.datePickerEntryMode = DatePickerEntryMode.calendar, this.timePickerEntryMode = TimePickerEntryMode.dial, this.createQuestionnaireAnswerFiller = _createDefaultAnswerFiller, - this.questionResponseItemLayoutBuilder = _defaultQuestionResponseItemLayoutBuilder, + this.questionResponseItemLayoutBuilder = + _defaultQuestionResponseItemLayoutBuilder, this.groupItemLayoutBuilder = _defaultGroupItemLayoutBuilder, this.displayItemLayoutBuilder = _defaultDisplayItemLayoutBuilder, this.codingControlLayoutBuilder = _defaultCodingControlLayoutBuilder, this.scrollerItemBuilder = _defaultScrollerItemBuilder, - this.stepperQuestionnaireItemFiller = _defaultStepperQuestionnaireItemFiller, + this.stepperQuestionnaireItemFiller = + _defaultStepperQuestionnaireItemFiller, this.stepperPageItemBuilder = _defaultStepperPageItemBuilder, }); @@ -191,8 +192,7 @@ class QuestionnaireThemeData { /// Used by [QuestionnaireResponseFiller]. QuestionnaireItemFiller createQuestionnaireItemFiller( QuestionnaireFillerData questionnaireFiller, - FillerItemModel fillerItemModel, - Locale locale, { + FillerItemModel fillerItemModel, { Key? key, }) { if (fillerItemModel is QuestionItemModel) { @@ -428,15 +428,14 @@ class QuestionnaireThemeData { QuestionnaireFillerData responseFiller, int index, ) { - return responseFiller.itemFillerAt(context, index); + return responseFiller.itemFillerAt(index); } static QuestionnaireItemFiller? _defaultStepperQuestionnaireItemFiller( - BuildContext context, QuestionnaireFillerData responseFiller, int index, ) { - final itemFiller = responseFiller.visibleItemFillerAt(context, index); + final itemFiller = responseFiller.visibleItemFillerAt(index); if (itemFiller == null) return null; return itemFiller; From 3598028e169d969b943d32e8b739ed21aebcca11 Mon Sep 17 00:00:00 2001 From: Hanny Prastya Hariyadi Date: Wed, 11 Oct 2023 20:57:04 +0900 Subject: [PATCH 19/23] Revert changes --- .../item/answer/src/coding_answer_option_model.dart | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/lib/questionnaires/model/item/answer/src/coding_answer_option_model.dart b/lib/questionnaires/model/item/answer/src/coding_answer_option_model.dart index 7c37bccd..9ba3117f 100644 --- a/lib/questionnaires/model/item/answer/src/coding_answer_option_model.dart +++ b/lib/questionnaires/model/item/answer/src/coding_answer_option_model.dart @@ -133,11 +133,7 @@ class CodingAnswerOptionModel { ?.valueString; final optionPrefix = (plainOptionPrefix != null) - ? RenderingString.fromText( - plainOptionPrefix, - locale: locale, - extensions: extensions, - ) + ? RenderingString.fromText(plainOptionPrefix) : null; RenderingString optionText; @@ -151,18 +147,13 @@ class CodingAnswerOptionModel { forDisplay = plainText; optionText = RenderingString.fromText( plainText, - locale: locale, extensions: xhtmlExtensions, ); } else { final plainText = _createMultiColumn(coding, locale, questionnaireItemModel); forDisplay = _createForDisplay(coding, locale, questionnaireItemModel); - optionText = RenderingString.fromText( - plainText, - locale: locale, - extensions: extensions, - ); + optionText = RenderingString.fromText(plainText); } } else { // The spec only allows valueCoding, but valueString occurs in the real world From 0f82ef052054f94c403434c34b87bc37010ec48d Mon Sep 17 00:00:00 2001 From: Hanny Prastya Hariyadi Date: Wed, 11 Oct 2023 20:57:29 +0900 Subject: [PATCH 20/23] Remove unused parameter --- lib/questionnaires/model/src/rendering_string.dart | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/questionnaires/model/src/rendering_string.dart b/lib/questionnaires/model/src/rendering_string.dart index c0494f30..1388235d 100644 --- a/lib/questionnaires/model/src/rendering_string.dart +++ b/lib/questionnaires/model/src/rendering_string.dart @@ -35,8 +35,6 @@ class RenderingString with Diagnosticable { /// The unaltered rendering-markdown extension final Markdown? renderingMarkdown; - final List? extensions; - /// Construct an [RenderingString] from the provided attributes. /// /// No alterations of the attributes will take place, in particular, @@ -48,7 +46,6 @@ class RenderingString with Diagnosticable { this.renderingStyle, this.renderingXhtml, this.renderingMarkdown, - this.extensions, }); /// Construct an [RenderingString] from plainText and optional extensions. @@ -114,7 +111,6 @@ class RenderingString with Diagnosticable { renderingStyle: renderingStyle, renderingXhtml: renderingXhtml, renderingMarkdown: renderingMarkdown, - extensions: extensions, ); } From 7c47cf7fd771fec2eb05b81c9c9a8f47eb5c6a16 Mon Sep 17 00:00:00 2001 From: Hanny Prastya Hariyadi Date: Wed, 11 Oct 2023 20:57:55 +0900 Subject: [PATCH 21/23] Add inline documentation --- example/lib/custom_questionnaire_stepper_page.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/example/lib/custom_questionnaire_stepper_page.dart b/example/lib/custom_questionnaire_stepper_page.dart index 5487fb97..44bb981e 100644 --- a/example/lib/custom_questionnaire_stepper_page.dart +++ b/example/lib/custom_questionnaire_stepper_page.dart @@ -41,6 +41,7 @@ class _CustomQuestionnaireStepperPageState return; } + // Validate the first matching item, and if it's valid, navigate to the next page final errors = matchingItems.first.validate(notifyListeners: true); if (errors.isEmpty) { _navigateToNextPage(); From 33aae122977430f71d8f89271e0a1f560a518856 Mon Sep 17 00:00:00 2001 From: Hanny Prastya Hariyadi Date: Thu, 12 Oct 2023 15:27:00 +0900 Subject: [PATCH 22/23] Remove unnecessary changes --- .../view/src/questionnaire_stepper_page_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/questionnaires/view/src/questionnaire_stepper_page_view.dart b/lib/questionnaires/view/src/questionnaire_stepper_page_view.dart index 999b664b..aa9274a8 100644 --- a/lib/questionnaires/view/src/questionnaire_stepper_page_view.dart +++ b/lib/questionnaires/view/src/questionnaire_stepper_page_view.dart @@ -32,7 +32,7 @@ class QuestionnaireStepperPageView extends StatefulWidget { class _QuestionnaireStepperPageViewState extends State { - PageController _pageController = PageController(keepPage: true); + PageController _pageController = PageController(); bool _hasRequestsRunning = false; QuestionnaireItemFiller? _currentQuestionnaireItemFiller; From e4c6b15a894b0a10e10a198dd7a4b166c4e8ebf9 Mon Sep 17 00:00:00 2001 From: Hanny Prastya Hariyadi Date: Thu, 12 Oct 2023 15:28:38 +0900 Subject: [PATCH 23/23] Add documentation --- lib/questionnaires/view/src/questionnaire_filler.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/questionnaires/view/src/questionnaire_filler.dart b/lib/questionnaires/view/src/questionnaire_filler.dart index 78f28ad8..1e5f3b33 100644 --- a/lib/questionnaires/view/src/questionnaire_filler.dart +++ b/lib/questionnaires/view/src/questionnaire_filler.dart @@ -144,6 +144,10 @@ class _QuestionnaireResponseFillerState return QuestionnaireLoadingIndicator(snapshot); } + // If the locale of `_questionnaireResponseModel` doesn't match the locale of `snapshot.data`, + // reset `_handleQuestionnaireResponseModelChangeListenerFunction` to null. + // This ensures that `_questionnaireFillerData` will be re-initialized + // using the most recent questionnaire response model that has the updated locale if (_questionnaireResponseModel?.locale != snapshot.data?.locale) { _handleQuestionnaireResponseModelChangeListenerFunction = null; }