Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<PhaseSequence> sequenceTypeControl = FormControl(
value: defaultScheduleType,
Expand Down Expand Up @@ -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.',
},
);

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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({
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,12 @@ class _StudyScheduleFormViewState extends State<StudyScheduleFormView> {
//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:
Expand Down
18 changes: 11 additions & 7 deletions designer_v2/lib/utils/input_formatter.dart
Original file line number Diff line number Diff line change
@@ -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});

Expand Down Expand Up @@ -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),
);
}
}
Original file line number Diff line number Diff line change
@@ -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');
},
);
});
}
28 changes: 28 additions & 0 deletions designer_v2/test/utils/input_formatter_test.dart
Original file line number Diff line number Diff line change
@@ -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');
});
});
}
Loading