Skip to content

Commit 52243e6

Browse files
committed
Implement row-level validation for newly added preparation rows
1 parent 5e0f88b commit 52243e6

7 files changed

Lines changed: 409 additions & 36 deletions

File tree

‎ios/Podfile.lock‎

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -215,33 +215,33 @@ SPEC CHECKSUMS:
215215
AppAuth: 1c1a8afa7e12f2ec3a294d9882dfa5ab7d3cb063
216216
AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f
217217
Firebase: aa154fee4e9b8eac17aa42344988865b3e857d33
218-
firebase_core: 9156a152117c843440b0b990c785aa0259bc5447
219-
firebase_messaging: 0d962ab44ff24ed36deb8fa2ee043c4671858269
218+
firebase_core: 40bc9f4c0ee3a28fbfb1e00e2ddaed744bb86388
219+
firebase_messaging: 7168ed5c5f52fc396426745be147263cfcc39ecc
220220
FirebaseCore: 86241206e656f5c80c995e370e6c975913b9b284
221221
FirebaseCoreInternal: 7c12fc3011d889085e765e317d7b9fd1cef97af9
222222
FirebaseInstallations: 4e6e162aa4abaaeeeb01dd00179dfc5ad9c2194e
223223
FirebaseMessaging: 341004946fa7ffc741344b20f1b667514fc93e31
224224
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
225-
flutter_appauth: d4abcf54856e5d8ba82ed7646ffc83245d4aa448
226-
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
227-
flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23
228-
google_sign_in_ios: 205742c688aea0e64db9da03c33121694a365109
225+
flutter_appauth: 88fcbc27871cbedac400db9d39b1363e4850179d
226+
flutter_local_notifications: ff50f8405aaa0ccdc7dcfb9022ca192e8ad9688f
227+
flutter_secure_storage_darwin: 557817588b80e60213cbecb573c45c76b788018d
228+
google_sign_in_ios: 4bb0e529b167cadc6ac785b6ed943c0a0a4cc1c9
229229
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
230230
GoogleSignIn: fcee2257188d5eda57a5e2b6a715550ffff9206d
231231
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
232232
GTMAppAuth: 217a876b249c3c585a54fd6f73e6b58c4f5c4238
233233
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
234-
kakao_flutter_sdk_common: 682b3606698f87467788598dc2dc09d4e6867fbd
234+
kakao_flutter_sdk_common: a21740b9dd4900f96161f365a2b6ece7a97cbf4f
235235
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
236-
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
237-
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
236+
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
237+
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
238238
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
239-
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
240-
sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418
239+
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
240+
sign_in_with_apple: f3bf75217ea4c2c8b91823f225d70230119b8440
241241
sqlite3: a51c07cf16e023d6c48abd5e5791a61a47354921
242-
sqlite3_flutter_libs: b3e120efe9a82017e5552a620f696589ed4f62ab
243-
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
244-
webview_flutter_wkwebview: 1821ceac936eba6f7984d89a9f3bcb4dea99ebb2
242+
sqlite3_flutter_libs: f9114e4bbe1f2e03dd543373c53d23245982ca13
243+
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
244+
webview_flutter_wkwebview: a4af96a051138e28e29f60101d094683b9f82188
245245

246246
PODFILE CHECKSUM: be4663332fca77601dd6736e3676fe47177084e1
247247

‎lib/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/bloc/preparation_form_bloc.dart‎

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ class PreparationFormBloc
2929
on<PreparationFormPreparationStepNameFocusLost>(
3030
_onPreparationFormPreparationStepNameFocusLost,
3131
);
32+
on<PreparationFormPreparationStepInteractionEnded>(
33+
_onPreparationFormPreparationStepInteractionEnded,
34+
);
3235
on<PreparationFormPreparationStepTimeChanged>(
3336
_onPreparationFormPreparationStepTimeChanged,
3437
);
@@ -122,10 +125,15 @@ class PreparationFormBloc
122125
PreparationFormPreparationStepNameChanged event,
123126
Emitter<PreparationFormState> emit,
124127
) {
128+
final step = state.preparationStepList.elementAtOrNull(event.index);
129+
if (step == null) {
130+
return;
131+
}
132+
125133
final changedList = List<PreparationStepFormState>.from(
126134
state.preparationStepList,
127135
);
128-
changedList[event.index] = changedList[event.index].copyWith(
136+
changedList[event.index] = step.copyWith(
129137
preparationName: PreparationNameInputModel.dirty(
130138
event.preparationStepName,
131139
),
@@ -182,14 +190,69 @@ class PreparationFormBloc
182190
);
183191
}
184192

193+
void _onPreparationFormPreparationStepInteractionEnded(
194+
PreparationFormPreparationStepInteractionEnded event,
195+
Emitter<PreparationFormState> emit,
196+
) {
197+
final step = state.preparationStepList.elementAtOrNull(event.index);
198+
if (step == null || step.id != state.addingStepId) {
199+
return;
200+
}
201+
202+
final isNameBlank = event.preparationStepName.trim().isEmpty;
203+
final isTimeBlank = step.preparationTime.value == Duration.zero;
204+
if (isNameBlank && isTimeBlank) {
205+
final changedList = List<PreparationStepFormState>.from(
206+
state.preparationStepList,
207+
)..removeAt(event.index);
208+
final isValid = _validate(changedList);
209+
emit(
210+
state.copyWith(
211+
preparationStepList: changedList,
212+
status: PreparationFormStatus.initial,
213+
clearAddingStepId: true,
214+
isValid: isValid,
215+
),
216+
);
217+
return;
218+
}
219+
220+
final changedStep = step.copyWith(
221+
preparationName: PreparationNameInputModel.dirty(
222+
event.preparationStepName,
223+
),
224+
preparationTime: PreparationTimeInputModel.dirty(
225+
step.preparationTime.value,
226+
),
227+
);
228+
final changedList = List<PreparationStepFormState>.from(
229+
state.preparationStepList,
230+
)..[event.index] = changedStep;
231+
final isValid = _validate(changedList);
232+
final shouldCommitAddingStep = _shouldCommitAddingStep(changedStep);
233+
emit(
234+
state.copyWith(
235+
preparationStepList: changedList,
236+
status: shouldCommitAddingStep ? PreparationFormStatus.initial : null,
237+
clearAddingStepId: shouldCommitAddingStep,
238+
isValid: isValid,
239+
),
240+
);
241+
}
242+
185243
void _onPreparationFormPreparationStepTimeChanged(
186244
PreparationFormPreparationStepTimeChanged event,
187245
Emitter<PreparationFormState> emit,
188246
) {
247+
final step = state.preparationStepList.elementAtOrNull(event.index);
248+
if (step == null) {
249+
return;
250+
}
251+
189252
final changedList = List<PreparationStepFormState>.from(
190253
state.preparationStepList,
191254
);
192-
changedList[event.index] = changedList[event.index].copyWith(
255+
changedList[event.index] = step.copyWith(
193256
preparationTime: PreparationTimeInputModel.dirty(
194257
event.preparationStepTime,
195258
),
@@ -261,6 +324,13 @@ class PreparationFormBloc
261324
);
262325
int oldIndex = event.oldIndex;
263326
int newIndex = event.newIndex;
327+
if (oldIndex < 0 ||
328+
oldIndex >= changedList.length ||
329+
newIndex < 0 ||
330+
newIndex > changedList.length) {
331+
return;
332+
}
333+
264334
if (oldIndex < newIndex) {
265335
newIndex -= 1;
266336
}

‎lib/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/bloc/preparation_form_event.dart‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,20 @@ final class PreparationFormPreparationStepNameFocusLost
6464
List<Object> get props => [index, preparationStepName];
6565
}
6666

67+
final class PreparationFormPreparationStepInteractionEnded
68+
extends PreparationFormEvent {
69+
final int index;
70+
final String preparationStepName;
71+
72+
const PreparationFormPreparationStepInteractionEnded({
73+
required this.index,
74+
required this.preparationStepName,
75+
});
76+
77+
@override
78+
List<Object> get props => [index, preparationStepName];
79+
}
80+
6781
final class PreparationFormPreparationStepTimeChanged
6882
extends PreparationFormEvent {
6983
final int index;

‎lib/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/components/preparation_form_list_field.dart‎

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@ class PreparationFormListField extends StatefulWidget {
2020
this.onNameFocusLost,
2121
this.onPreparationTimeChanged,
2222
this.onPreparationTimeTapped,
23+
this.onInteractionEnded,
2324
this.onRemove,
2425
this.onNameSaved,
2526
this.canRemove = true,
2627
this.isAdding = false,
2728
this.showValidationErrors = false,
2829
this.focusNode,
30+
this.swipeActionController,
2931
});
3032

3133
final PreparationStepFormState preparationStep;
@@ -34,12 +36,14 @@ class PreparationFormListField extends StatefulWidget {
3436
final ValueChanged<String>? onNameFocusLost;
3537
final ValueChanged<Duration>? onPreparationTimeChanged;
3638
final VoidCallback? onPreparationTimeTapped;
39+
final ValueChanged<String>? onInteractionEnded;
3740
final VoidCallback? onRemove;
3841
final VoidCallback? onNameSaved;
3942
final bool canRemove;
4043
final bool isAdding;
4144
final bool showValidationErrors;
4245
final FocusNode? focusNode;
46+
final SwipeActionController? swipeActionController;
4347

4448
@override
4549
State<PreparationFormListField> createState() =>
@@ -48,9 +52,11 @@ class PreparationFormListField extends StatefulWidget {
4852

4953
class _PreparationFormListFieldState extends State<PreparationFormListField> {
5054
late final FocusNode _internalFocusNode;
51-
final SwipeActionController _swipeActionController = SwipeActionController();
55+
final SwipeActionController _internalSwipeActionController =
56+
SwipeActionController();
5257
late String _nameValue;
5358
bool _hasRequestedInitialFocus = false;
59+
bool _isTimePickerOpen = false;
5460
final dragIndicatorSvg = SvgPicture.asset(
5561
'drag_indicator.svg',
5662
package: 'assets',
@@ -63,7 +69,7 @@ class _PreparationFormListFieldState extends State<PreparationFormListField> {
6369
@override
6470
void dispose() {
6571
_effectiveFocusNode.removeListener(_handleFocusChanged);
66-
_swipeActionController.dispose();
72+
_internalSwipeActionController.dispose();
6773
_internalFocusNode.dispose();
6874
super.dispose();
6975
}
@@ -80,6 +86,12 @@ class _PreparationFormListFieldState extends State<PreparationFormListField> {
8086
@override
8187
void didUpdateWidget(covariant PreparationFormListField oldWidget) {
8288
super.didUpdateWidget(oldWidget);
89+
final oldFocusNode = oldWidget.focusNode ?? _internalFocusNode;
90+
final currentFocusNode = widget.focusNode ?? _internalFocusNode;
91+
if (oldFocusNode != currentFocusNode) {
92+
oldFocusNode.removeListener(_handleFocusChanged);
93+
currentFocusNode.addListener(_handleFocusChanged);
94+
}
8395
if (oldWidget.preparationStep.id != widget.preparationStep.id) {
8496
_nameValue = widget.preparationStep.preparationName.value;
8597
}
@@ -91,6 +103,9 @@ class _PreparationFormListFieldState extends State<PreparationFormListField> {
91103

92104
FocusNode get _effectiveFocusNode => widget.focusNode ?? _internalFocusNode;
93105

106+
SwipeActionController get _swipeActionController =>
107+
widget.swipeActionController ?? _internalSwipeActionController;
108+
94109
void _requestInitialFocusIfNeeded() {
95110
if (!widget.isAdding || _hasRequestedInitialFocus) {
96111
return;
@@ -104,11 +119,44 @@ class _PreparationFormListFieldState extends State<PreparationFormListField> {
104119
}
105120

106121
void _handleFocusChanged() {
122+
_swipeActionController.closeAllOpenCell();
107123
if (!_effectiveFocusNode.hasFocus) {
108-
_swipeActionController.closeAllOpenCell();
109-
widget.onNameFocusLost?.call(_nameValue);
110-
widget.onNameSaved?.call();
124+
_handleRowInteractionEnded();
125+
}
126+
}
127+
128+
void _handleRowInteractionEnded([String? nameValue]) {
129+
final value = nameValue ?? _nameValue;
130+
if (widget.isAdding && _isTimePickerOpen) {
131+
return;
132+
}
133+
if (widget.isAdding) {
134+
widget.onInteractionEnded?.call(value);
135+
return;
136+
}
137+
widget.onNameFocusLost?.call(value);
138+
widget.onNameSaved?.call();
139+
}
140+
141+
void _handlePreparationTimeTapped() {
142+
_swipeActionController.closeAllOpenCell();
143+
if (widget.isAdding) {
144+
_isTimePickerOpen = true;
145+
return;
146+
}
147+
widget.onPreparationTimeTapped?.call();
148+
}
149+
150+
void _handlePreparationTimePickerDisposed() {
151+
_isTimePickerOpen = false;
152+
if (!widget.isAdding) {
153+
return;
111154
}
155+
WidgetsBinding.instance.addPostFrameCallback((_) {
156+
if (mounted && widget.isAdding) {
157+
_effectiveFocusNode.requestFocus();
158+
}
159+
});
112160
}
113161

114162
String? _nameErrorText(BuildContext context) {
@@ -168,7 +216,8 @@ class _PreparationFormListFieldState extends State<PreparationFormListField> {
168216
trailing: PreparationTimeInput(
169217
time: widget.preparationStep.preparationTime.value,
170218
hasError: timeErrorText != null,
171-
onTap: widget.onPreparationTimeTapped,
219+
onTap: _handlePreparationTimeTapped,
220+
onDisposed: _handlePreparationTimePickerDisposed,
172221
onPreparationTimeChanged: widget.onPreparationTimeChanged,
173222
),
174223
child: Expanded(
@@ -188,9 +237,9 @@ class _PreparationFormListFieldState extends State<PreparationFormListField> {
188237
},
189238
onFieldSubmitted: (value) {
190239
_nameValue = value;
191-
widget.onNameFocusLost?.call(value);
192-
widget.onNameSaved?.call();
240+
_handleRowInteractionEnded(value);
193241
},
242+
onTap: _swipeActionController.closeAllOpenCell,
194243
onTapOutside: (event) {
195244
_swipeActionController.closeAllOpenCell();
196245
FocusManager.instance.primaryFocus?.unfocus();

0 commit comments

Comments
 (0)