diff --git a/designer_v2/lib/features/design/interventions/study_schedule_form_controller_mixin.dart b/designer_v2/lib/features/design/interventions/study_schedule_form_controller_mixin.dart index 8ddde0667..d33d9ea90 100644 --- a/designer_v2/lib/features/design/interventions/study_schedule_form_controller_mixin.dart +++ b/designer_v2/lib/features/design/interventions/study_schedule_form_controller_mixin.dart @@ -6,12 +6,14 @@ import 'package:studyu_designer_v2/features/design/study_form_validation.dart'; import 'package:studyu_designer_v2/features/forms/form_validation.dart'; import 'package:studyu_designer_v2/features/forms/form_view_model.dart'; import 'package:studyu_designer_v2/localization/app_translation.dart'; +import 'package:studyu_designer_v2/utils/input_formatter.dart'; mixin StudyScheduleControls { static const defaultScheduleType = PhaseSequence.alternating; static const defaultScheduleTypeSequence = 'ABAB'; static const defaultNumCycles = 2; static const defaultPeriodLength = 7; + static final RegExp customSequencePattern = RegExp(r'^[ABab]+$'); final FormControl sequenceTypeControl = FormControl( value: defaultScheduleType, @@ -110,10 +112,15 @@ mixin StudyScheduleControls { FormControlValidation get customSequenceRequired => FormControlValidation( control: sequenceTypeCustomControl, - validators: [Validators.required], + validators: [ + Validators.required, + Validators.pattern(customSequencePattern), + ], validationMessages: { ValidationMessage.required: (error) => 'Custom sequence needs to be specified.', + ValidationMessage.pattern: (error) => + 'Custom sequence can only contain A and B.', }, ); @@ -128,7 +135,9 @@ mixin StudyScheduleControls { StudyScheduleFormData buildStudyScheduleFormData() { return StudyScheduleFormData( sequenceType: sequenceTypeControl.value!, // required - sequenceTypeCustom: sequenceTypeCustomControl.value!, // required + sequenceTypeCustom: normalizeStudySequenceInput( + sequenceTypeCustomControl.value!, + ), // required numCycles: numCyclesControl.value!, // required phaseDuration: phaseDurationControl.value!, // required includeBaseline: includeBaselineControl.value!, // required diff --git a/designer_v2/lib/features/design/interventions/study_schedule_form_data.dart b/designer_v2/lib/features/design/interventions/study_schedule_form_data.dart index 1763004ea..cdf014a69 100644 --- a/designer_v2/lib/features/design/interventions/study_schedule_form_data.dart +++ b/designer_v2/lib/features/design/interventions/study_schedule_form_data.dart @@ -1,6 +1,7 @@ import 'package:studyu_core/core.dart'; import 'package:studyu_designer_v2/features/design/study_form_data.dart'; import 'package:studyu_designer_v2/features/forms/form_data.dart'; +import 'package:studyu_designer_v2/utils/input_formatter.dart'; class StudyScheduleFormData implements IStudyFormData { StudyScheduleFormData({ @@ -30,7 +31,7 @@ class StudyScheduleFormData implements IStudyFormData { StudySchedule toStudySchedule() { final schedule = StudySchedule(); schedule.sequence = sequenceType; - schedule.sequenceCustom = sequenceTypeCustom; + schedule.sequenceCustom = normalizeStudySequenceInput(sequenceTypeCustom); schedule.numberOfCycles = numCycles; schedule.phaseDuration = phaseDuration; schedule.includeBaseline = includeBaseline; diff --git a/designer_v2/lib/features/design/interventions/study_schedule_form_view.dart b/designer_v2/lib/features/design/interventions/study_schedule_form_view.dart index 1daf5d281..f07ff1947 100644 --- a/designer_v2/lib/features/design/interventions/study_schedule_form_view.dart +++ b/designer_v2/lib/features/design/interventions/study_schedule_form_view.dart @@ -74,9 +74,12 @@ class _StudyScheduleFormViewState extends State { //formControl: widget.formViewModel.sequenceTypeControl, onChanged: widget.formViewModel.sequenceTypeControl.disabled ? null - : (PhaseSequence? value) => + : (PhaseSequence? value) { + setState(() { widget.formViewModel.sequenceTypeControl.value = - value, + value; + }); + }, initialValue: widget.formViewModel.sequenceTypeControl.value, decoration: InputDecoration( helperText: diff --git a/designer_v2/lib/utils/input_formatter.dart b/designer_v2/lib/utils/input_formatter.dart index b5e924295..123e6713a 100644 --- a/designer_v2/lib/utils/input_formatter.dart +++ b/designer_v2/lib/utils/input_formatter.dart @@ -1,5 +1,8 @@ import 'package:flutter/services.dart'; +String normalizeStudySequenceInput(String value) => + value.replaceAll(RegExp(r'\s+'), '').toUpperCase(); + class NumericalRangeFormatter extends TextInputFormatter { NumericalRangeFormatter({this.min, this.max}); @@ -30,14 +33,15 @@ class StudySequenceFormatter extends TextInputFormatter { TextEditingValue oldValue, TextEditingValue newValue, ) { - if (newValue.text == '') { - return newValue.copyWith(text: newValue.text.toUpperCase()); - } else if (newValue.text - .replaceAll(' ', '') - .contains(RegExp(r'^[abAB]+$'))) { - return newValue.copyWith(text: newValue.text.toUpperCase()); - } else { + final normalized = normalizeStudySequenceInput(newValue.text); + + if (normalized.isNotEmpty && !RegExp(r'^[AB]+$').hasMatch(normalized)) { return oldValue; } + + return TextEditingValue( + text: normalized, + selection: TextSelection.collapsed(offset: normalized.length), + ); } } diff --git a/designer_v2/test/features/design/interventions/study_schedule_form_data_test.dart b/designer_v2/test/features/design/interventions/study_schedule_form_data_test.dart new file mode 100644 index 000000000..2c8a9f3b7 --- /dev/null +++ b/designer_v2/test/features/design/interventions/study_schedule_form_data_test.dart @@ -0,0 +1,22 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:studyu_core/core.dart'; +import 'package:studyu_designer_v2/features/design/interventions/study_schedule_form_data.dart'; + +void main() { + group('StudyScheduleFormData', () { + test( + 'normalizes custom sequence before applying it to the domain model', + () { + final data = StudyScheduleFormData( + sequenceType: PhaseSequence.customized, + sequenceTypeCustom: ' a b B a ', + numCycles: 2, + phaseDuration: 7, + includeBaseline: true, + ); + + expect(data.toStudySchedule().sequenceCustom, 'ABBA'); + }, + ); + }); +} diff --git a/designer_v2/test/utils/input_formatter_test.dart b/designer_v2/test/utils/input_formatter_test.dart new file mode 100644 index 000000000..f40f63400 --- /dev/null +++ b/designer_v2/test/utils/input_formatter_test.dart @@ -0,0 +1,28 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:studyu_designer_v2/utils/input_formatter.dart'; + +void main() { + group('StudySequenceFormatter', () { + final formatter = StudySequenceFormatter(); + + TextEditingValue format(String oldText, String newText) { + return formatter.formatEditUpdate( + TextEditingValue(text: oldText), + TextEditingValue(text: newText), + ); + } + + test('uppercases valid sequence input', () { + expect(format('', 'abba').text, 'ABBA'); + }); + + test('removes whitespace from pasted sequence input', () { + expect(format('', ' A b B A ').text, 'ABBA'); + }); + + test('rejects characters other than A and B', () { + expect(format('AB', 'ABC').text, 'AB'); + }); + }); +}