diff --git a/example/lib/custom_questionnaire_stepper_page.dart b/example/lib/custom_questionnaire_stepper_page.dart index cc1d9910..44bb981e 100644 --- a/example/lib/custom_questionnaire_stepper_page.dart +++ b/example/lib/custom_questionnaire_stepper_page.dart @@ -1,4 +1,5 @@ import 'package:faiadashu/faiadashu.dart'; +import 'package:faiadashu_example/main.dart'; import 'package:flutter/material.dart'; class CustomQuestionnaireStepperPage extends StatefulWidget { @@ -41,7 +42,8 @@ class _CustomQuestionnaireStepperPageState } // Validate the first matching item, and if it's valid, navigate to the next page - if (matchingItems.first.validate(notifyListeners: true) == null) { + final errors = matchingItems.first.validate(notifyListeners: true); + if (errors.isEmpty) { _navigateToNextPage(); } } @@ -67,8 +69,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 +110,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..29851e01 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) { @@ -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/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'), + ), ), ], ); diff --git a/example/lib/questionnaire_launch_tile.dart b/example/lib/questionnaire_launch_tile.dart index d8f04e96..a296285a 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,14 @@ class _QuestionnaireLaunchTileState extends State { ); } - Future _createModelFuture() { + Future _createModelFuture( + BuildContext context, + ) { return QuestionnaireResponseModel.fromFhirResourceBundle( + localizations: FDashLocalizations.of(context), fhirResourceProvider: _questionnaireProvider, launchContext: widget.launchContext, - locale: _locale, + locale: Localizations.localeOf(context), ).then((qrm) { qrm.populate( widget.restoreResponseFunction.call(widget.questionnairePath), @@ -74,9 +77,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(context); } @override @@ -134,62 +138,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 +231,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(context); }); }); }, 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 f1bf5769..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, @@ -76,8 +74,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 +111,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 +157,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/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..c40679f6 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/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 39019a45..62ca5b9d 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..4678c53b 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,18 @@ 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); + /// + /// 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. /// - /// Returns null when it is valid, or a localized message when it is not. - String? 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. /// @@ -93,29 +95,27 @@ 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, - ); + final validationError = validateValue(value); - if (errorText == newErrorText) { - return newErrorText; + if (_validationError == validationError && validationError != null) { + return [validationError]; } if (updateErrorText) { - errorText = newErrorText; + _validationError = validationError; } + if (notifyListeners) { this.notifyListeners(); } - return newErrorText; + return _validationError != null ? [_validationError!] : []; } /// Returns whether any answer (valid or invalid) has been provided. @@ -124,12 +124,14 @@ abstract class AnswerModel extends ResponseNode { /// Returns whether this question is unanswered. bool get isEmpty; - String? errorText; + ValidationError? _validationError; /// 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) => + _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 ffeab84e..5eb32e91 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,9 @@ 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:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; import 'package:fhir/r4.dart'; -import 'package:filesize/filesize.dart'; class AttachmentAnswerModel extends AnswerModel { final num maxSize; @@ -10,14 +11,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 +30,26 @@ class AttachmentAnswerModel extends AnswerModel { : RenderingString.nullText; @override - String? validateInput(Attachment? inValue) { + ValidationError? validateInput(Attachment? inValue) { return validateValue(inValue); } @override - String? 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) { - return lookupFDashLocalizations(locale).validatorMaxSize(filesize(maxSize)); + return 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)) { + 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 076a379c..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 - String? validateInput(Boolean? inValue) { + ValidationError? validateInput(Boolean? inValue) { return null; } @override - String? 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 b38e86e0..c6709511 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,10 @@ 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/model/src/validation_errors/validation_error.dart'; import 'package:faiadashu/questionnaires/questionnaires.dart'; import 'package:fhir/r4.dart'; @@ -154,15 +158,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 +226,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,12 +307,12 @@ class CodingAnswerModel extends AnswerModel { } @override - String? validateInput(OptionsOrString? inValue) { + ValidationError? validateInput(OptionsOrString? inValue) { return validateValue(inValue); } @override - String? validateValue(OptionsOrString? inValue) { + ValidationError? validateValue(OptionsOrString? inValue) { if (inValue == null) { return null; } @@ -318,18 +324,17 @@ class CodingAnswerModel extends AnswerModel { if (!(questionnaireItemModel.questionnaireItem.repeats?.value ?? false)) { if (totalCount != 1) { - return lookupFDashLocalizations(locale) - .validatorSingleSelectionOrSingleOpenString(openLabel.plainText); + return SingleSelectionOrOpenStringError(nodeUid, _openLabel); } } if (totalCount < minOccurs) { - return lookupFDashLocalizations(locale).validatorMinOccurs(minOccurs); + return MinOccursError(nodeUid, minOccurs); } final maxOccurs = this.maxOccurs; if (maxOccurs != null && totalCount > maxOccurs) { - return lookupFDashLocalizations(locale).validatorMaxOccurs(maxOccurs); + return MaxOccursError(nodeUid, maxOccurs); } return null; 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..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 @@ -75,6 +75,7 @@ class CodingAnswerOptionModel { final plainText = coding.localizedDisplay(locale); optionText = RenderingString.fromText( plainText, + locale: locale, extensions: coding.displayElement?.extension_, ); forDisplay = plainText; @@ -152,9 +153,7 @@ class CodingAnswerOptionModel { final plainText = _createMultiColumn(coding, locale, questionnaireItemModel); forDisplay = _createForDisplay(coding, locale, questionnaireItemModel); - optionText = RenderingString.fromText( - plainText, - ); + optionText = RenderingString.fromText(plainText); } } else { // The spec only allows valueCoding, but valueString occurs in the real world @@ -169,6 +168,7 @@ class CodingAnswerOptionModel { forDisplay = plainText; optionText = RenderingString.fromText( plainText, + locale: locale, extensions: xhtmlExtensions, ); } 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..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/l10n/l10n.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 - String? validateInput(FhirDateTime? inValue) { + ValidationError? validateInput(FhirDateTime? inValue) { return validateValue(inValue); } @override - String? validateValue(FhirDateTime? inValue) { - return inValue == null || inValue.isValid - ? null - : lookupFDashLocalizations(locale).validatorDateTime; + ValidationError? validateValue(FhirDateTime? inValue) { + if (!(inValue == null || inValue.isValid)) { + return DateTimeError(nodeUid); + } + return null; } @override @@ -81,13 +83,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..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,8 +1,11 @@ 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/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'; @@ -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,7 +189,7 @@ class NumericalAnswerModel extends AnswerModel { : RenderingString.nullText; @override - String? validateInput(String? inputValue) { + ValidationError? validateInput(String? inputValue) { if (inputValue == null || inputValue.isEmpty) { return null; } @@ -191,8 +199,8 @@ class NumericalAnswerModel extends AnswerModel { } catch (_) { // Ignore FormatException, number remains nan. } - if (number == double.nan) { - return lookupFDashLocalizations(locale).validatorNan; + if (number.isNaN) { + return NanError(nodeUid); } final quantity = _valueFromNumber(number); @@ -201,7 +209,7 @@ class NumericalAnswerModel extends AnswerModel { } @override - String? validateValue(Quantity? inputValue) { + ValidationError? 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)); + return MaxValueError(nodeUid, Decimal(_maxValue).format(locale)); } if (number < _minValue) { - return lookupFDashLocalizations(locale) - .validatorMinValue(Decimal(_minValue).format(locale)); + return MinValueError(nodeUid, Decimal(_minValue).format(locale)); } return null; @@ -259,8 +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) == null; - final dataAbsentReasonExtension = !valid + final valid = validateInput(textInput); + + 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 d8b11ab0..d0b48dfd 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,11 @@ 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:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart'; import 'package:fhir/r4.dart'; enum StringAnswerKeyboard { plain, email, phone, number, multiline, url } @@ -61,38 +65,37 @@ class StringAnswerModel extends AnswerModel { : RenderingString.nullText; @override - String? validateInput(String? inValue) { + ValidationError? validateInput(String? inValue) { final checkValue = inValue?.trim(); return validateValue(checkValue); } @override - String? validateValue(String? inputValue) { + ValidationError? validateValue(String? inputValue) { if (inputValue == null || inputValue.isEmpty) { return null; } if (inputValue.length < minLength) { - return lookupFDashLocalizations(locale).validatorMinLength(minLength); + return MinLengthError(nodeUid, minLength); } if (maxLength != null && inputValue.length > maxLength!) { - return lookupFDashLocalizations(locale).validatorMaxLength(maxLength!); + return MinLengthError(nodeUid, maxLength!); } if (qi.type == QuestionnaireItemType.url) { if (!_urlRegExp.hasMatch(inputValue)) { - return lookupFDashLocalizations(locale).validatorUrl; + return UrlError(nodeUid); } } if (regExp != null) { if (!regExp!.hasMatch(inputValue)) { return (entryFormat != null) - ? lookupFDashLocalizations(locale) - .validatorEntryFormat(entryFormat!) - : lookupFDashLocalizations(locale).validatorRegExp; + ? EntryFormatError(nodeUid, entryFormat!) + : RegexError(nodeUid); } } @@ -106,6 +109,7 @@ class StringAnswerModel extends AnswerModel { final value = this.value?.trim(); final valid = validateInput(value) == null; + final dataAbsentReasonExtension = !valid ? [ FhirExtension( 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..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 - String? validateInput(Object? inValue) { + ValidationError? validateInput(Object? inValue) { return null; } @override - String? 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 76ed1df1..163168b1 100644 --- a/lib/questionnaires/model/item/src/question_item_model.dart +++ b/lib/questionnaires/model/item/src/question_item_model.dart @@ -1,10 +1,9 @@ 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/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'; import 'package:fhir_path/fhir_path.dart'; @@ -172,36 +171,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 +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(); + 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 b48ead2e..b069b2b7 100644 --- a/lib/questionnaires/model/item/src/response_item_model.dart +++ b/lib/questionnaires/model/item/src/response_item_model.dart @@ -1,6 +1,7 @@ -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/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,46 +49,41 @@ 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 validationError?.getMessage(localizations); + } + + ValidationError? validationError; - Map? validate({ + List validate({ bool updateErrorText = true, bool notifyListeners = false, }) { - String? newErrorText; + ValidationError? newValidationError; if (questionnaireItemModel.isRequired && isUnanswered) { - newErrorText = lookupFDashLocalizations(questionnaireResponseModel.locale) - .validatorRequiredItem; + newValidationError = RequiredItemError(nodeUid); } final constraintError = validateConstraint(); - newErrorText ??= constraintError; + newValidationError ??= constraintError; - if (errorText != newErrorText) { + if (validationError != newValidationError) { if (updateErrorText) { - errorText = newErrorText; + validationError = newValidationError; } if (notifyListeners) { this.notifyListeners(); } } - if (newErrorText == null) { - return null; - } else { - final resultMap = {}; - resultMap[nodeUid] = newErrorText; - - return resultMap; - } + return newValidationError != null ? [newValidationError] : []; } /// 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() { + /// Returns [ValidationError] if not satisfied; otherwise null + ValidationError? validateConstraint() { final constraintExpression = _constraintExpression; if (constraintExpression == null) { return null; @@ -99,6 +95,11 @@ abstract class ResponseItemModel extends FillerItemModel { location: nodeUid, ); - return isSatisfied ? null : questionnaireItemModel.constraintHuman; + return isSatisfied + ? null + : ConstraintValidationError( + nodeUid, + questionnaireItemModel.constraintHuman, + ); } } 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/questionnaire_response_model.dart b/lib/questionnaires/model/src/questionnaire_response_model.dart index c8b3315e..8032187d 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, ); @@ -832,8 +833,9 @@ 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) { + return fim.nodeUid == uid; + }); if (fillerItem != null) { return fillerItem; } @@ -853,27 +855,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({ + /// 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()) { - final errorTexts = itemModel.validate( + final errors = itemModel.validate( updateErrorText: updateErrorText, notifyListeners: notifyListeners, ); - if (errorTexts != null) { - _logger.debug('$itemModel is invalid.'); - - invalidMap.addAll(errorTexts); - } + 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/model/src/rendering_string.dart b/lib/questionnaires/model/src/rendering_string.dart index 7a05287b..1388235d 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 @@ -59,6 +61,7 @@ class RenderingString with Diagnosticable { /// * rendering-xhtml factory RenderingString.fromText( String plainText, { + Locale? locale, List? extensions, String? xhtmlText, }) { @@ -80,7 +83,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 @@ -95,7 +99,7 @@ class RenderingString with Diagnosticable { ) : (renderingStyle != null) ? '$escapedPlainText' - : plainText; + : text; return RenderingString._( plainText, diff --git a/lib/questionnaires/model/src/validation_errors/common_validation_error.dart b/lib/questionnaires/model/src/validation_errors/common_validation_error.dart new file mode 100644 index 00000000..77b3dbab --- /dev/null +++ b/lib/questionnaires/model/src/validation_errors/common_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 CommonValidationError extends ValidationError { + final String? message; + const CommonValidationError(String nodeUid, this.message) : super(nodeUid); + + @override + String? getMessage(FDashLocalizations localizations) { + return message!; + } +} 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/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/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); +} 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..0490f22d 100644 --- a/lib/questionnaires/view/item/answer/src/coding_answer_filler.dart +++ b/lib/questionnaires/view/item/answer/src/coding_answer_filler.dart @@ -43,12 +43,15 @@ 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, + openStringInputControlWidget: answerModel.isOptionsOrString + ? _OpenStringInputControl(answerModel) + : null, errorText: errorText, ); } @@ -314,7 +317,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 +475,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 +596,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 +620,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 +656,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..1b098571 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; @@ -56,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), @@ -65,7 +65,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..457ec33a 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,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'; @@ -184,6 +185,8 @@ class _NumberFieldInputControl ); } + final localizations = FDashLocalizations.of(context); + return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -195,7 +198,7 @@ class _NumberFieldInputControl textAlignVertical: TextAlignVertical.center, textAlign: TextAlign.end, decoration: InputDecoration( - errorText: answerModel.displayErrorText, + errorText: answerModel.displayErrorText(localizations), errorStyle: (itemModel .isCalculated) // Force display of error text on calculated item ? TextStyle( @@ -206,9 +209,10 @@ class _NumberFieldInputControl prefixIcon: itemModel.isCalculated ? Icon( Icons.calculate, - color: (answerModel.displayErrorText != null) - ? Theme.of(context).errorColor - : null, + color: + (answerModel.displayErrorText(localizations) != null) + ? Theme.of(context).errorColor + : null, ) : null, suffixIcon: (answerModel.hasUnitChoices) @@ -225,7 +229,9 @@ class _NumberFieldInputControl validator: (itemModel.isCalculated) ? null : (inputValue) { - return answerModel.validateInput(inputValue); + return answerModel + .validateInput(inputValue) + ?.getMessage(localizations); }, autovalidateMode: (itemModel.isCalculated) ? AutovalidateMode.disabled @@ -237,11 +243,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 +282,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..7e072838 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,4 @@ +import 'package:faiadashu/l10n/l10n.dart'; import 'package:faiadashu/questionnaires/questionnaires.dart'; import 'package:fhir/r4.dart'; import 'package:flutter/material.dart'; @@ -60,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) @@ -94,7 +96,7 @@ class _StringAnswerInputControl extends AnswerInputControl { ? QuestionnaireTheme.of(context).maxLinesForTextItem : 1, decoration: InputDecoration( - errorText: answerModel.displayErrorText, + errorText: answerModel.displayErrorText(locale), errorStyle: (itemModel .isCalculated) // Force display of error text on calculated item ? TextStyle( @@ -105,13 +107,14 @@ class _StringAnswerInputControl extends AnswerInputControl { prefixIcon: itemModel.isCalculated ? Icon( Icons.calculate, - color: (answerModel.displayErrorText != null) + color: (answerModel.displayErrorText(locale) != null) ? Theme.of(context).errorColor : null, ) : null, ), - validator: (inputValue) => answerModel.validateInput(inputValue), + validator: (inputValue) => + answerModel.validateInput(inputValue)?.getMessage(locale), 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_title.dart b/lib/questionnaires/view/item/src/questionnaire_item_filler_title.dart index f0a25ce8..c0f4f76f 100644 --- a/lib/questionnaires/view/item/src/questionnaire_item_filler_title.dart +++ b/lib/questionnaires/view/item/src/questionnaire_item_filler_title.dart @@ -48,10 +48,11 @@ class QuestionnaireItemFillerTitle extends StatelessWidget { : '

'; final prefixText = fillerItem.prefix; + final title = text.xhtmlText; final htmlTitleText = (prefixText != null) - ? '$openStyleTag${prefixText.xhtmlText} ${text.xhtmlText}$requiredTag$closeStyleTag' - : '$openStyleTag${text.xhtmlText}$requiredTag$closeStyleTag'; + ? '$openStyleTag${prefixText.xhtmlText} ${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..665106b2 100644 --- a/lib/questionnaires/view/src/questionnaire_complete_button.dart +++ b/lib/questionnaires/view/src/questionnaire_complete_button.dart @@ -31,10 +31,16 @@ class _QuestionnaireCompleteButtonState final currentResponseStatus = qrm.responseStatus; if (currentResponseStatus != QuestionnaireResponseStatus.completed) { - final incompleteItems = qrm.validate(notifyListeners: true); - qrm.invalidityNotifier.value = incompleteItems; + final validationErrors = qrm.validate( + notifyListeners: true, + ); + qrm.invalidityNotifier.value = { + for (var validation in validationErrors) + validation.nodeUid: + validation.getMessage(FDashLocalizations.of(context)) ?? "" + }; - if (incompleteItems != null) { + if (validationErrors.isNotEmpty) { return; } } diff --git a/lib/questionnaires/view/src/questionnaire_filler.dart b/lib/questionnaires/view/src/questionnaire_filler.dart index babc906d..1e5f3b33 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: @@ -152,6 +144,13 @@ 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; + } if (snapshot.hasData) { _logger.debug('FutureBuilder hasData'); _questionnaireResponseModel = snapshot.data; @@ -168,7 +167,6 @@ class _QuestionnaireResponseFillerState _questionnaireFillerData = QuestionnaireFillerData._( _questionnaireResponseModel!, - locale: widget.locale, builder: widget.builder, onLinkTap: widget.onLinkTap, onDataAvailable: widget.onDataAvailable, @@ -191,7 +189,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 +203,6 @@ class QuestionnaireFillerData extends InheritedWidget { QuestionnaireFillerData._( this.questionnaireResponseModel, { Key? key, - required this.locale, this.onDataAvailable, this.onLinkTap, required this.questionnaireTheme, @@ -260,8 +256,9 @@ class QuestionnaireFillerData extends InheritedWidget { /// see: https://en.wikipedia.org/wiki/Tree_traversal#Pre-order,_NLR QuestionnaireItemFiller itemFillerAt(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, @@ -284,11 +281,13 @@ 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 @@ -306,8 +305,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..3b9d7698 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,9 @@ class _QuestionnaireScrollerState extends State { @override Widget build(BuildContext context) { - final locale = widget.locale ?? Localizations.localeOf(context); - return QuestionnaireResponseFiller( fhirResourceProvider: widget.fhirResourceProvider, launchContext: widget.launchContext, - locale: locale, questionnaireModelDefaults: widget.questionnaireModelDefaults, builder: (BuildContext context) { _belowFillerContext = context; @@ -170,50 +165,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, + ), ), - const Spacer(), - ], - ); - }, - ); - }, - ), + child: + QuestionnaireTheme.of(context).scrollerItemBuilder( + context, + QuestionnaireResponseFiller.of(context), + i, + ), + ), + 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..aa9274a8 100644 --- a/lib/questionnaires/view/src/questionnaire_stepper_page_view.dart +++ b/lib/questionnaires/view/src/questionnaire_stepper_page_view.dart @@ -30,7 +30,8 @@ class QuestionnaireStepperPageView extends StatefulWidget { _QuestionnaireStepperPageViewState(); } -class _QuestionnaireStepperPageViewState extends State { +class _QuestionnaireStepperPageViewState + extends State { PageController _pageController = PageController(); bool _hasRequestsRunning = false; QuestionnaireItemFiller? _currentQuestionnaireItemFiller; @@ -74,8 +75,10 @@ class _QuestionnaireStepperPageViewState extends State