From d8d8ac62c2e4d8902619a0fda089c53306e016dc Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Thu, 24 Sep 2026 07:48:51 +0300 Subject: [PATCH] fix(mongodb): guard document editor Refresh against unsaved JSON edits Check _dirty before reloading in _reloadFromServer and prompt the user with confirmDiscardUnsavedWorkIfNeeded(). If the user cancels, the reload is aborted and their edits are preserved. Also defer the didUpdateWidget reload via addPostFrameCallback to ensure the dialog context is fully mounted before showing. Test: new case 'dirty Refresh Cancel keeps edits; Discard reloads from server' Closes #870 --- .../mongodb/mongo_document_editor.dart | 9 +- .../mongodb/mongo_document_editor_test.dart | 90 +++++++++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) diff --git a/lib/features/mongodb/mongo_document_editor.dart b/lib/features/mongodb/mongo_document_editor.dart index f183485..fa60288 100644 --- a/lib/features/mongodb/mongo_document_editor.dart +++ b/lib/features/mongodb/mongo_document_editor.dart @@ -66,7 +66,9 @@ class _MongoDocumentEditorState extends material.State { void didUpdateWidget(covariant MongoDocumentEditor oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.refreshToken != widget.refreshToken) { - _reloadFromServer(); + material.WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) unawaited(_reloadFromServer()); + }); } } @@ -74,6 +76,11 @@ class _MongoDocumentEditorState extends material.State { Future _reloadFromServer() async { final id = widget.document['_id']; if (id == null) return; + if (_dirty) { + if (!await confirmDiscardUnsavedWorkIfNeeded(context)) return; + if (!mounted) return; + setState(() => _dirty = false); + } setState(() { _error = null; _success = null; diff --git a/test/features/mongodb/mongo_document_editor_test.dart b/test/features/mongodb/mongo_document_editor_test.dart index 1cb72c2..772ee6f 100644 --- a/test/features/mongodb/mongo_document_editor_test.dart +++ b/test/features/mongodb/mongo_document_editor_test.dart @@ -173,4 +173,94 @@ void main() { ); expect(UnsavedWorkRegistry.instance.hasUnsaved, isFalse); }); + + testWidgets( + 'dirty Refresh Cancel keeps edits; Discard reloads from server', + (tester) async { + await tester.binding.setSurfaceSize(const material.Size(800, 700)); + + await tester.pumpWidget( + queryaThemeTestShell( + child: material.SizedBox( + width: 800, + height: 700, + child: MongoDocumentEditor( + connection: connection, + database: 'db', + collection: 'items', + document: const {'_id': 'abc', 'a': 1}, + refreshToken: 0, + ), + ), + ), + ); + await tester.pumpAndSettle(); + await pumpSyntaxHighlightDebounce(tester); + + await tester.enterText( + find.byType(material.EditableText), + '{"_id":"abc","a":999}', + ); + await tester.pump(); + expect(UnsavedWorkRegistry.instance.hasUnsaved, isTrue); + + // Bump refreshToken (simulate breadcrumb toolbar Refresh) + await tester.pumpWidget( + queryaThemeTestShell( + child: material.SizedBox( + width: 800, + height: 700, + child: MongoDocumentEditor( + connection: connection, + database: 'db', + collection: 'items', + document: const {'_id': 'abc', 'a': 1}, + refreshToken: 1, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + // Dialog must appear + expect(find.text('Unsaved changes'), findsOneWidget); + + // Cancel must keep edits + await tester.tap(find.text('Cancel')); + await tester.pumpAndSettle(); + + final editable = tester.widget( + find.byType(material.EditableText), + ); + expect(editable.controller.text, contains('999')); + expect(UnsavedWorkRegistry.instance.hasUnsaved, isTrue); + + // Bump refreshToken again + await tester.pumpWidget( + queryaThemeTestShell( + child: material.SizedBox( + width: 800, + height: 700, + child: MongoDocumentEditor( + connection: connection, + database: 'db', + collection: 'items', + document: const {'_id': 'abc', 'a': 1}, + refreshToken: 2, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.text('Unsaved changes'), findsOneWidget); + await tester.tap(find.text('Discard')); + await tester.pumpAndSettle(); + + expect(UnsavedWorkRegistry.instance.hasUnsaved, isFalse); + + await tester.pumpWidget( + queryaThemeTestShell(child: const material.SizedBox()), + ); + }); }