Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- **Mongo document editor Back (#782)** — Dirty JSON in `MongoDocumentEditor` is registered with `UnsavedWorkRegistry`. Breadcrumb Back, Home, Close, and tree navigation confirm before discarding; Cancel keeps the editor.
- **Mongo JSON filter ObjectId / DateTime (#783)** — Document list filter parses Extended JSON (`$oid`, `$date`) and wraps a 24-character hex `_id` as `ObjectId`. A leftover string `_id` with zero matches shows how to write `{ "_id": { "$oid": "…" } }`.
- **Postgres open-transaction probe (#787)** — `BEGIN` + `SELECT` does not assign an XID, so `pg_current_xact_id_if_assigned` stayed NULL (SQL-tab badge off; autocommit-off prepended a second `BEGIN`). The session tracks BEGIN/COMMIT/ROLLBACK and otherwise probes `pg_stat_activity.xact_start` (PG 9+). Implicit BEGIN when autocommit is off stays a separate `execute`.
- **Postgres Table Browser paging (#788)** — Browse stays on `PgSessionMode.readOnly`. `SELECT` uses `ORDER BY` primary-key columns when a PK exists. Row totals come from `pg_class.reltuples` instead of a blocking `COUNT(*)` before first paint (stale estimates below the current page are ignored so Next still works). Save / REFRESH stay on `tableWrite`.
Expand Down
11 changes: 6 additions & 5 deletions lib/core/unsaved_work_guard.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import 'package:flutter/material.dart' as material;
import 'package:querya_desktop/core/unsaved_work_registry.dart';
import 'package:querya_desktop/shared/widgets/widgets.dart';

/// Confirms before Home / Close / tree navigation discards SQL or staged grid
/// edits. Returns true when it is safe to tear down the current view.
/// Confirms before Home / Close / tree navigation discards SQL, staged grid
/// edits, or a dirty document editor. Returns true when it is safe to tear
/// down the current view.
Future<bool> confirmDiscardUnsavedWorkIfNeeded(
material.BuildContext context,
) async {
Expand All @@ -12,7 +13,7 @@ Future<bool> confirmDiscardUnsavedWorkIfNeeded(
return confirmed == true;
}

/// Prompt when leaving a workspace that has unsaved SQL or table edits.
/// Prompt when leaving a workspace that has unsaved SQL, table, or document edits.
Future<bool?> showDiscardUnsavedWorkDialog(material.BuildContext context) {
return showAppDialog<bool>(
context: context,
Expand All @@ -28,8 +29,8 @@ Future<bool?> showDiscardUnsavedWorkDialog(material.BuildContext context) {
const Text('Unsaved changes').semiBold().large(),
const Gap(8),
const Text(
'You have unsaved SQL or staged table edits. '
'Continuing will discard them.',
'You have unsaved SQL, staged table edits, or document '
'changes. Continuing will discard them.',
).muted().small(),
const Gap(20),
material.Align(
Expand Down
2 changes: 1 addition & 1 deletion lib/core/unsaved_work_registry.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'package:flutter/foundation.dart';

/// Process-wide probes for unsaved SQL / staged grid edits.
/// Process-wide probes for unsaved SQL, staged grid edits, and document editors.
///
/// Used by the in-app updater (and similar quit paths) to warn before
/// discarding work. Owners register a probe and must unregister on dispose.
Expand Down
14 changes: 13 additions & 1 deletion lib/features/mongodb/mongo_document_editor.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import 'dart:async' show unawaited;

import 'package:flutter/material.dart' as material;
import 'package:querya_desktop/core/database/destructive_sql_detector.dart';
import 'package:querya_desktop/core/database/mongodb_connection.dart';
import 'package:querya_desktop/core/database/mongodb_service.dart';
import 'package:querya_desktop/core/editor/querya_code_editor.dart';
import 'package:querya_desktop/core/editor/querya_code_language.dart';
import 'package:querya_desktop/core/unsaved_work_guard.dart';
import 'package:querya_desktop/core/unsaved_work_registry.dart';
import 'package:querya_desktop/features/mongodb/mongo_ejson.dart';
import 'package:querya_desktop/features/workspace/destructive_query_dialog.dart';
import 'package:querya_desktop/shared/widgets/widgets.dart';
Expand Down Expand Up @@ -55,6 +59,7 @@ class _MongoDocumentEditorState extends material.State<MongoDocumentEditor> {
text: mongoDocumentToEjson(widget.document),
);
_controller.addListener(_onTextChanged);
UnsavedWorkRegistry.instance.register(this, () => _dirty || _saving);
}

@override
Expand Down Expand Up @@ -102,11 +107,18 @@ class _MongoDocumentEditorState extends material.State<MongoDocumentEditor> {

@override
void dispose() {
UnsavedWorkRegistry.instance.unregister(this);
_controller.removeListener(_onTextChanged);
_controller.dispose();
super.dispose();
}

Future<void> _onBack() async {
if (!await confirmDiscardUnsavedWorkIfNeeded(context)) return;
if (!mounted) return;
widget.onBack?.call();
}

void _onTextChanged() {
if (!_dirty) {
setState(() => _dirty = true);
Expand Down Expand Up @@ -237,7 +249,7 @@ class _MongoDocumentEditorState extends material.State<MongoDocumentEditor> {
child: Row(
children: [
material.InkWell(
onTap: widget.onBack,
onTap: () => unawaited(_onBack()),
borderRadius: material.BorderRadius.circular(6),
child: material.Padding(
padding: const material.EdgeInsets.all(4),
Expand Down
14 changes: 12 additions & 2 deletions lib/features/mongodb/mongo_explorer_view.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import 'dart:async' show unawaited;

import 'package:flutter/material.dart' as material;
import 'package:querya_desktop/core/database/mongodb_connection.dart';
import 'package:querya_desktop/core/database/mongodb_service.dart';
import 'package:querya_desktop/core/motion/querya_switching_body.dart';
import 'package:querya_desktop/core/storage/local_db.dart';
import 'package:querya_desktop/core/unsaved_work_guard.dart';
import 'package:querya_desktop/shared/widgets/widgets.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart' as shadcn;

Expand Down Expand Up @@ -192,7 +195,14 @@ class _MongoExplorerViewState extends material.State<MongoExplorerView> {
return list;
}

void _onCrumbTap(_Crumb crumb) {
Future<void> _onCrumbTap(_Crumb crumb) async {
final leavesDocument = _selectedDocument != null &&
crumb.level != _Level.document &&
crumb.level != _Level.stats;
if (leavesDocument) {
if (!await confirmDiscardUnsavedWorkIfNeeded(context)) return;
if (!mounted) return;
}
if (_showStats && crumb.level != _Level.stats) {
setState(() => _showStats = false);
}
Expand Down Expand Up @@ -275,7 +285,7 @@ class _MongoExplorerViewState extends material.State<MongoExplorerView> {
// Breadcrumb bar
_BreadcrumbBar(
crumbs: _crumbs,
onCrumbTap: _onCrumbTap,
onCrumbTap: (crumb) => unawaited(_onCrumbTap(crumb)),
onRefresh: () => setState(() => _refreshToken++),
onStats: () => setState(() => _showStats = !_showStats),
),
Expand Down
4 changes: 2 additions & 2 deletions lib/features/updater/update_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ Future<bool?> showUnsavedUpdateRestartDialog(material.BuildContext context) {
const Text('Unsaved changes').semiBold().large(),
const Gap(8),
const Text(
'You have unsaved SQL or staged table changes. '
'Restarting to install the update will discard them.',
'You have unsaved SQL, staged table edits, or document '
'changes. Restarting to install the update will discard them.',
).muted().small(),
const Gap(20),
material.Align(
Expand Down
50 changes: 50 additions & 0 deletions test/features/mongodb/mongo_document_editor_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:querya_desktop/core/database/mongodb_connection.dart';
import 'package:querya_desktop/core/editor/querya_code_editor.dart';
import 'package:querya_desktop/core/editor/syntax_highlight_service.dart';
import 'package:querya_desktop/core/theme/querya_theme.dart';
import 'package:querya_desktop/core/unsaved_work_registry.dart';
import 'package:querya_desktop/features/mongodb/mongo_document_editor.dart';

import '../../support/pump_syntax_highlight.dart';
Expand All @@ -15,12 +16,15 @@ void main() {
await SyntaxHighlightService.ensureInitialized();
});

tearDown(UnsavedWorkRegistry.instance.resetForTest);

final connection = MongoConnection(id: 1, name: 'test', host: 'localhost');

Future<void> pumpEditor(
WidgetTester tester, {
QueryaTheme? theme,
Map<String, dynamic> document = const {'_id': 'abc', 'a': 1},
material.VoidCallback? onBack,
}) async {
await tester.pumpWidget(
queryaThemeTestShell(
Expand All @@ -33,6 +37,7 @@ void main() {
database: 'db',
collection: 'items',
document: document,
onBack: onBack,
),
),
),
Expand Down Expand Up @@ -123,4 +128,49 @@ void main() {
expect(deleted, isFalse);
expect(find.text('Delete'), findsOneWidget);
});

testWidgets('clean Back leaves the editor without a confirm dialog',
(tester) async {
var wentBack = false;
await pumpEditor(tester, onBack: () => wentBack = true);
expect(UnsavedWorkRegistry.instance.hasUnsaved, isFalse);

await tester.tap(find.byIcon(material.Icons.arrow_back_rounded));
await tester.pumpAndSettle();

expect(wentBack, isTrue);
expect(find.text('Unsaved changes'), findsNothing);
});

testWidgets('dirty Back Cancel keeps edits; Discard calls onBack',
(tester) async {
await tester.binding.setSurfaceSize(const material.Size(800, 700));
var wentBack = false;
await pumpEditor(tester, onBack: () => wentBack = true);

await tester.enterText(find.byType(material.EditableText), '{"a":2}');
await tester.pump();
expect(UnsavedWorkRegistry.instance.hasUnsaved, isTrue);

await tester.tap(find.byIcon(material.Icons.arrow_back_rounded));
await tester.pumpAndSettle();
expect(find.text('Unsaved changes'), findsOneWidget);
expect(wentBack, isFalse);

await tester.tap(find.text('Cancel'));
await tester.pumpAndSettle();
expect(wentBack, isFalse);
expect(UnsavedWorkRegistry.instance.hasUnsaved, isTrue);

await tester.tap(find.byIcon(material.Icons.arrow_back_rounded));
await tester.pumpAndSettle();
await tester.tap(find.text('Discard'));
await tester.pumpAndSettle();
expect(wentBack, isTrue);

await tester.pumpWidget(
queryaThemeTestShell(child: const material.SizedBox()),
);
expect(UnsavedWorkRegistry.instance.hasUnsaved, isFalse);
});
}
Loading