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
6 changes: 1 addition & 5 deletions designer_v2/lib/common_views/form_buttons.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,7 @@ List<Widget> buildFormButtons(FormViewModel formViewModel, FormMode formMode) {
// enable re-rendering based on form validation status
builder: (context, form, child) {
return retainSizeInAppBar(
DismissButton(
onPressed: () => formViewModel.cancel().then((_) {
if (context.mounted) Navigator.maybePop(context);
}),
),
DismissButton(onPressed: () => Navigator.maybePop(context)),
);
},
),
Expand Down
24 changes: 14 additions & 10 deletions designer_v2/lib/common_views/sidesheet/sidesheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ Future<T?> showModalSideSheet<T extends Object?>({
String? barrierLabel = "Sidesheet",
bool useRootNavigator = true,
RouteSettings? routeSettings,
Widget Function(Widget)? wrapRoute,
}) {
assert(!barrierDismissible || barrierLabel != null);
return showGeneralDialog(
Expand All @@ -259,16 +260,19 @@ Future<T?> showModalSideSheet<T extends Object?>({
useRootNavigator: useRootNavigator,
routeSettings: routeSettings,
context: context,
pageBuilder: (BuildContext context, _, _) => Sidesheet(
body: body,
tabs: tabs,
wrapContent: wrapContent,
width: width,
withCloseButton: withCloseButton,
ignoreAppBar: ignoreAppBar,
actionButtons: actionButtons,
titleText: title,
),
pageBuilder: (BuildContext context, _, _) {
final sidesheet = Sidesheet(
body: body,
tabs: tabs,
wrapContent: wrapContent,
width: width,
withCloseButton: withCloseButton,
ignoreAppBar: ignoreAppBar,
actionButtons: actionButtons,
titleText: title,
);
return wrapRoute != null ? wrapRoute(sidesheet) : sidesheet;
},
transitionBuilder: (_, animation, _, child) {
return SlideTransition(
position: Tween<Offset>(
Expand Down
108 changes: 107 additions & 1 deletion designer_v2/lib/common_views/sidesheet/sidesheet_form.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:reactive_forms/reactive_forms.dart';
import 'package:studyu_designer_v2/common_views/form_buttons.dart';
import 'package:studyu_designer_v2/common_views/form_scaffold.dart';
import 'package:studyu_designer_v2/common_views/navbar_tabbed.dart';
import 'package:studyu_designer_v2/common_views/sidesheet/sidesheet.dart';
import 'package:studyu_designer_v2/features/forms/form_view_model.dart';
import 'package:studyu_designer_v2/features/forms/unsaved_changes_dialog.dart';
import 'package:studyu_designer_v2/theme.dart';

class FormSideSheetTab<T extends FormViewModel> extends NavbarTab {
Expand All @@ -20,6 +22,108 @@ class FormSideSheetTab<T extends FormViewModel> extends NavbarTab {
FormViewBuilder<T> formViewBuilder;
}

class _FormSidesheetPopEntry<T extends FormViewModel> extends StatefulWidget {
const _FormSidesheetPopEntry({
required this.formViewModel,
required this.child,
});

final T formViewModel;
final Widget child;

@override
State<_FormSidesheetPopEntry<T>> createState() =>
_FormSidesheetPopEntryState<T>();
}

class _FormSidesheetPopEntryState<T extends FormViewModel>
extends State<_FormSidesheetPopEntry<T>>
implements PopEntry {
ModalRoute<dynamic>? _route;
final ValueNotifier<bool> _canPopNotifier = ValueNotifier(false);

@override
void didChangeDependencies() {
super.didChangeDependencies();
_route?.unregisterPopEntry(this);
_route = ModalRoute.of(context);
debugPrint('[PopEntry] registering with route: ${_route.runtimeType}');
_route?.registerPopEntry(this);
}

@override
void dispose() {
_route?.unregisterPopEntry(this);
_route = null;
_canPopNotifier.dispose();
super.dispose();
}

@override
ValueListenable<bool> get canPopNotifier => _canPopNotifier;

@override
void onPopInvokedWithResult(bool didPop, Object? result) {
debugPrint(
'[PopEntry] onPopInvokedWithResult didPop=$didPop canPop=${_canPopNotifier.value}',
);
if (didPop) {
return;
}
_handleDismiss();
}

@override
void onPopInvoked(bool didPop) {
// Deprecated
}

Future<void> _handleDismiss() async {
debugPrint(
'[PopEntry] _handleDismiss isDirty=${widget.formViewModel.isDirty}',
);

if (!widget.formViewModel.isDirty) {
await widget.formViewModel.cancel();
if (mounted) {
_canPopNotifier.value = true;

// CHANGE HERE: Wait for the frame to finish so Navigator is unlocked
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
Navigator.of(context, rootNavigator: true).pop();
}
});
}
return;
}

// Dirty state logic
final shouldDiscard = await showDialog<bool>(
context: context,
barrierColor: ThemeConfig.modalBarrierColor(Theme.of(context)),
builder: (context) => const UnsavedChangesDialog(),
);

if (shouldDiscard == true && mounted) {
await widget.formViewModel.cancel();
if (mounted && Navigator.of(context).canPop()) {
_canPopNotifier.value = true;

// CHANGE HERE: Wait for the frame to finish
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
Navigator.of(context, rootNavigator: true).pop();
}
});
}
}
}

@override
Widget build(BuildContext context) => widget.child;
}

Future<Object?> showFormSideSheet<T extends FormViewModel>({
required BuildContext context,
required T formViewModel,
Expand All @@ -29,7 +133,7 @@ Future<Object?> showFormSideSheet<T extends FormViewModel>({
double? width,
bool withCloseButton = false,
bool ignoreAppBar = true,
bool barrierDismissible = false,
bool barrierDismissible = true,
Color? barrierColor,
}) {
barrierColor ??= ThemeConfig.modalBarrierColor(Theme.of(context));
Expand Down Expand Up @@ -65,5 +169,7 @@ Future<Object?> showFormSideSheet<T extends FormViewModel>({
ignoreAppBar: ignoreAppBar,
barrierDismissible: barrierDismissible,
barrierColor: barrierColor,
wrapRoute: (sidesheet) =>
_FormSidesheetPopEntry(formViewModel: formViewModel, child: sidesheet),
);
}
46 changes: 33 additions & 13 deletions designer_v2/lib/features/forms/form_view_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ abstract class FormViewModel<T> implements IFormGroupController {
_restoreControlsFromFormData();
_formModeUpdated();
_applyValidationSet(validationSet);
Future.microtask(() {
finalizeInitializationBaseline();
});

if (autosave) {
// Push to event queue to avoid listening to update events
Expand All @@ -73,6 +76,11 @@ abstract class FormViewModel<T> implements IFormGroupController {
}
}

void finalizeInitializationBaseline() {
prevFormValue = _getFullFormValue();
form.markAsPristine();
}

T? get formData => _formData;
set formData(T? formData) => _setFormData(formData);
T? _formData;
Expand Down Expand Up @@ -133,17 +141,10 @@ abstract class FormViewModel<T> implements IFormGroupController {
/// values are initialized in [setControlsFrom] (controls that are set
/// programmatically are incorrectly marked as dirty without any user input).
bool get isDirty {
_rememberDefaultControlStates();

for (final control in form.controls.values) {
control.markAsEnabled(emitEvent: false, updateParent: false);
}
final isEqual = jsonEncode(prevFormValue) == jsonEncode(form.value);
if (prevFormValue == null) return false;

for (final control in form.controls.values) {
control.markAsEnabled(emitEvent: false, updateParent: false);
}
_restoreControlStates(emitEvent: false, updateParent: false);
final currentFormValue = _getFullFormValue();
final isEqual = jsonEncode(prevFormValue) == jsonEncode(currentFormValue);

return !isEqual;
}
Expand All @@ -169,10 +170,29 @@ abstract class FormViewModel<T> implements IFormGroupController {
void _setFormData(T? formData) {
_formData = formData;
if (formData != null) {
setControlsFrom(formData); // update [form] controls automatically
setControlsFrom(formData);
}
prevFormValue = {...form.value};
form.updateValueAndValidity();
finalizeInitializationBaseline();
}

JsonMap _getFullFormValue() {
_rememberDefaultControlStates();

// 1. Temporarily enable all controls
for (final control in form.controls.values) {
control.markAsEnabled(emitEvent: false, updateParent: false);
}
// 2. CRITICAL: Force the FormGroup to rebuild its value cache!
form.updateValueAndValidity(updateParent: false, emitEvent: false);

// 3. Deep copy the full value
final fullValue = jsonDecode(jsonEncode(form.value)) as JsonMap;

// 4. Restore original states and rebuild the cache again
_restoreControlStates(emitEvent: false, updateParent: false);
form.updateValueAndValidity(updateParent: false, emitEvent: false);

return fullValue;
}

void _rememberDefaultControlStates() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ class InviteCodeFormViewModel extends FormViewModel<StudyInvite> {
@override
void initControls() {
regenerateCode(); // initialize randomly
prevFormValue = {...form.value};
}

// - Validation
Expand Down
2 changes: 1 addition & 1 deletion flutter_common/lib/envs/.env.dev
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
STUDYU_SUPABASE_URLS=https://studyu-02.dhc-lab.hpi.de
STUDYU_SUPABASE_PUBLIC_ANON_KEY=eyJhbGciOiAiSFMyNTYiLCJ0eXAiOiAiSldUIn0.eyJyb2xlIjogImFub24iLCJpc3MiOiAic3VwYWJhc2UiLCJpYXQiOiAxNzY1MDEzMTIyLCJleHAiOiAxNzk2NTQ5MTIyfQ.d8cyoFZ_E3ymZyKEjZCP1VktnlMFntOaaosSwjjPFMY
STUDYU_SUPABASE_PUBLIC_ANON_KEY=eyJhbGciOiAiSFMyNTYiLCJ0eXAiOiAiSldUIn0.eyJyb2xlIjogImFub24iLCJpc3MiOiAic3VwYWJhc2UiLCJpYXQiOiAxNzU3Mzk3MDI5LCJleHAiOiAxNzg4OTMzMDI5fQ.BQwOLCX6h7RBvZ0xTdt2o-3Hw6YJxalI7EuCVwH69yo
STUDYU_PROJECT_GENERATOR_URL=
STUDYU_APP_URL=https://app.dev.studyu.health
STUDYU_DESIGNER_URL=https://designer.dev.studyu.health
Expand Down
Loading