From f6d402814658829e62cb29ccc7f90e7732a192d3 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Tue, 10 Mar 2026 06:00:33 +0300 Subject: [PATCH 1/7] Fix folder deletion and improve MongoDB connection form UI Made-with: Cursor --- .flutter-plugins-dependencies | 2 +- lib/core/storage/local_db.dart | 33 +++- .../mongodb/mongodb_connection_form.dart | 163 ++++++++++++------ 3 files changed, 143 insertions(+), 55 deletions(-) diff --git a/.flutter-plugins-dependencies b/.flutter-plugins-dependencies index 038e6a28..8acd192c 100644 --- a/.flutter-plugins-dependencies +++ b/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"path_provider_foundation","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_foundation-2.6.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\sqflite_darwin-2.4.2\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"path_provider_android","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_android-2.2.22\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_android","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\sqflite_android-2.4.2+2\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"bitsdojo_window_macos","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\bitsdojo_window_macos-0.1.4\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_foundation-2.6.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\sqflite_darwin-2.4.2\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"bitsdojo_window_linux","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\bitsdojo_window_linux-0.1.4\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_linux-2.2.1\\\\","native_build":false,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"bitsdojo_window_windows","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\bitsdojo_window_windows-0.1.6\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_windows-2.3.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false}],"web":[]},"dependencyGraph":[{"name":"bitsdojo_window","dependencies":["bitsdojo_window_windows","bitsdojo_window_macos","bitsdojo_window_linux"]},{"name":"bitsdojo_window_linux","dependencies":[]},{"name":"bitsdojo_window_macos","dependencies":[]},{"name":"bitsdojo_window_windows","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"sqflite","dependencies":["sqflite_android","sqflite_darwin"]},{"name":"sqflite_android","dependencies":[]},{"name":"sqflite_darwin","dependencies":[]}],"date_created":"2026-03-08 13:00:51.851139","version":"3.38.5","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"path_provider_foundation","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_foundation-2.6.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\sqflite_darwin-2.4.2\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"path_provider_android","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_android-2.2.22\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_android","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\sqflite_android-2.4.2+2\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"bitsdojo_window_macos","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\bitsdojo_window_macos-0.1.4\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_foundation-2.6.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\sqflite_darwin-2.4.2\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"bitsdojo_window_linux","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\bitsdojo_window_linux-0.1.4\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_linux-2.2.1\\\\","native_build":false,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"bitsdojo_window_windows","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\bitsdojo_window_windows-0.1.6\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_windows-2.3.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false}],"web":[]},"dependencyGraph":[{"name":"bitsdojo_window","dependencies":["bitsdojo_window_windows","bitsdojo_window_macos","bitsdojo_window_linux"]},{"name":"bitsdojo_window_linux","dependencies":[]},{"name":"bitsdojo_window_macos","dependencies":[]},{"name":"bitsdojo_window_windows","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"sqflite","dependencies":["sqflite_android","sqflite_darwin"]},{"name":"sqflite_android","dependencies":[]},{"name":"sqflite_darwin","dependencies":[]}],"date_created":"2026-03-10 05:45:19.165964","version":"3.41.4","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file diff --git a/lib/core/storage/local_db.dart b/lib/core/storage/local_db.dart index afdea28b..e7dbda98 100644 --- a/lib/core/storage/local_db.dart +++ b/lib/core/storage/local_db.dart @@ -5,7 +5,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; const _dbName = 'querya.db'; -const _dbVersion = 2; +const _dbVersion = 3; /// Local SQLite database for folders and connections. /// File: [applicationSupport]/querya_desktop/querya.db @@ -60,7 +60,7 @@ class LocalDb { auth_source TEXT, use_ssl INTEGER NOT NULL DEFAULT 0, connection_string TEXT, - folder_id INTEGER REFERENCES folders(id), + folder_id INTEGER REFERENCES folders(id) ON DELETE CASCADE, sort_order INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL ) @@ -75,6 +75,35 @@ class LocalDb { await db.execute('ALTER TABLE connections ADD COLUMN use_ssl INTEGER NOT NULL DEFAULT 0'); await db.execute('ALTER TABLE connections ADD COLUMN connection_string TEXT'); } + if (oldVersion < 3) { + await db.execute('PRAGMA foreign_keys=ON'); + await db.execute(''' + CREATE TABLE connections_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type TEXT NOT NULL, + name TEXT NOT NULL, + host TEXT, + port INTEGER, + username TEXT, + password TEXT, + database_name TEXT, + auth_source TEXT, + use_ssl INTEGER NOT NULL DEFAULT 0, + connection_string TEXT, + folder_id INTEGER REFERENCES folders(id) ON DELETE CASCADE, + sort_order INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL + ) + '''); + await db.execute(''' + INSERT INTO connections_new + SELECT id, type, name, host, port, username, password, database_name, + auth_source, use_ssl, connection_string, folder_id, sort_order, created_at + FROM connections + '''); + await db.execute('DROP TABLE connections'); + await db.execute('ALTER TABLE connections_new RENAME TO connections'); + } } Future> getFolders() async { diff --git a/lib/features/mongodb/mongodb_connection_form.dart b/lib/features/mongodb/mongodb_connection_form.dart index 2e35a13c..05c22318 100644 --- a/lib/features/mongodb/mongodb_connection_form.dart +++ b/lib/features/mongodb/mongodb_connection_form.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart' as material; import 'package:querya_desktop/core/database/mongodb_connection.dart'; import 'package:querya_desktop/core/storage/local_db.dart'; @@ -75,6 +77,8 @@ class _MongoConnectionFormContentState extends material.State<_MongoConnectionFo bool _useSSL = false; bool _showPassword = false; bool _isTesting = false; + String? _testResult; + Timer? _dismissTimer; @override void initState() { @@ -92,6 +96,7 @@ class _MongoConnectionFormContentState extends material.State<_MongoConnectionFo @override void dispose() { + _dismissTimer?.cancel(); _nameController.removeListener(_onFieldChanged); _hostController.removeListener(_onFieldChanged); _portController.removeListener(_onFieldChanged); @@ -121,9 +126,26 @@ class _MongoConnectionFormContentState extends material.State<_MongoConnectionFo : _connectionStringController.text.trim(), ); - String? _testResult; + void _showTestResult(String result) { + _dismissTimer?.cancel(); + setState(() { + _isTesting = false; + _testResult = result; + }); + _dismissTimer = Timer(const Duration(seconds: 5), () { + if (mounted) setState(() => _testResult = null); + }); + } + + void _dismissResult() { + _dismissTimer?.cancel(); + _dismissTimer = null; + setState(() => _testResult = null); + } Future _testConnection() async { + _dismissTimer?.cancel(); + _dismissTimer = null; setState(() { _isTesting = true; _testResult = null; @@ -147,19 +169,9 @@ class _MongoConnectionFormContentState extends material.State<_MongoConnectionFo final success = await connection.testConnection(); await connection.disconnect(); - if (mounted) { - setState(() { - _isTesting = false; - _testResult = success ? 'success' : 'failed'; - }); - } + if (mounted) _showTestResult(success ? 'success' : 'failed'); } catch (e) { - if (mounted) { - setState(() { - _isTesting = false; - _testResult = 'error: $e'; - }); - } + if (mounted) _showTestResult('error: $e'); } } @@ -387,55 +399,102 @@ class _MongoConnectionFormContentState extends material.State<_MongoConnectionFo ), const material.Divider(height: 1), if (_testResult != null) - material.Container( - padding: const material.EdgeInsets.symmetric(horizontal: 24, vertical: 8), - color: _testResult == 'success' - ? material.Colors.green.withValues(alpha: 0.1) - : material.Colors.red.withValues(alpha: 0.1), - child: Row( - children: [ - material.Icon( - _testResult == 'success' - ? material.Icons.check_circle_rounded - : material.Icons.error_rounded, - size: 16, - color: _testResult == 'success' - ? material.Colors.green - : material.Colors.red, - ), - const Gap(8), - material.Expanded( - child: Text( - _testResult == 'success' - ? 'Connection successful!' - : _testResult!.startsWith('error:') - ? _testResult!.substring(7) - : 'Connection failed', - ).small(), + material.Padding( + padding: const material.EdgeInsets.fromLTRB(24, 8, 16, 8), + child: material.Material( + color: material.Colors.transparent, + child: material.InkWell( + onTap: _dismissResult, + borderRadius: material.BorderRadius.circular(8), + child: material.Container( + padding: const material.EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: material.BoxDecoration( + color: _testResult == 'success' + ? theme.primary.withValues(alpha: 0.12) + : theme.destructive.withValues(alpha: 0.12), + borderRadius: material.BorderRadius.circular(8), + border: material.Border.all( + color: _testResult == 'success' + ? theme.primary.withValues(alpha: 0.35) + : theme.destructive.withValues(alpha: 0.35), + width: 1, + ), + ), + child: material.Row( + children: [ + material.Icon( + _testResult == 'success' + ? material.Icons.check_circle_outline + : material.Icons.info_outline_rounded, + size: 18, + color: _testResult == 'success' + ? theme.primary + : theme.destructive, + ), + const Gap(10), + material.Expanded( + child: Text( + _testResult == 'success' + ? 'Connection successful!' + : _testResult!.startsWith('error:') + ? _testResult!.substring(7) + : 'Connection failed', + style: material.TextStyle( + fontSize: 13, + color: theme.foreground, + ), + ).small(), + ), + material.IconButton( + icon: material.Icon( + material.Icons.close, + size: 18, + color: theme.mutedForeground, + ), + onPressed: _dismissResult, + style: material.IconButton.styleFrom( + minimumSize: const material.Size(28, 28), + padding: material.EdgeInsets.zero, + ), + ), + ], + ), ), - ], + ), ), ), material.Container( padding: const material.EdgeInsets.symmetric(horizontal: 24, vertical: 16), child: material.Row( - mainAxisAlignment: material.MainAxisAlignment.end, children: [ - GhostButton( - onPressed: () => material.Navigator.of(context).pop(), - child: const Text('Cancel'), - ), - const Gap(12), OutlineButton( - onPressed: _isTesting ? null : _testConnection, + onPressed: _formData.isValid && !_isTesting ? _testConnection : null, leading: _isTesting - ? const material.SizedBox( - width: 16, - height: 16, - child: material.CircularProgressIndicator(strokeWidth: 2), + ? material.SizedBox( + width: 18, + height: 18, + child: material.CircularProgressIndicator( + strokeWidth: 2, + color: theme.primary, + ), ) - : const material.Icon(material.Icons.check_circle_outline, size: 18), - child: const Text('Test Connection'), + : material.Icon( + material.Icons.link_rounded, + size: 18, + color: _formData.isValid ? theme.primary : theme.mutedForeground, + ), + child: Text( + 'Test Connection', + style: material.TextStyle( + fontWeight: material.FontWeight.w500, + color: _formData.isValid ? theme.primary : theme.mutedForeground, + ), + ), + ), + const material.Spacer(), + GhostButton( + onPressed: () => material.Navigator.of(context).pop(), + child: const Text('Cancel'), ), const Gap(12), PrimaryButton( From a3fc1b5f696ca4490a61d98d1e217e3a9a83208d Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Tue, 10 Mar 2026 06:05:57 +0300 Subject: [PATCH 2/7] Add MongoDB database tree view in sidebar (similar to Redis) Made-with: Cursor --- .../connections/connections_panel.dart | 334 +++++++++++++++++- lib/features/main_screen/main_screen.dart | 15 + lib/features/main_screen/workspace_panel.dart | 24 +- lib/features/mongodb/mongo_explorer_view.dart | 10 +- 4 files changed, 363 insertions(+), 20 deletions(-) diff --git a/lib/features/connections/connections_panel.dart b/lib/features/connections/connections_panel.dart index 6d79ec72..9f4b7bb4 100644 --- a/lib/features/connections/connections_panel.dart +++ b/lib/features/connections/connections_panel.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart' as material show Padding, Container, BoxDecoration, Border, BorderSide, InkWell, Icon, Icons, IconData, Image, EdgeInsets, BorderRadius, CrossAxisAlignment, MainAxisSize, MouseRegion, SystemMouseCursors, DefaultTextStyle, TextStyle, CustomScrollView, SliverToBoxAdapter, SliverFillRemaining, SliverPadding, GestureDetector, HitTestBehavior, SizedBox, Column, AnimatedRotation, Row, BoxFit, Text, TextOverflow, Expanded, CircularProgressIndicator; +import 'package:querya_desktop/core/database/mongodb_connection.dart'; import 'package:querya_desktop/core/database/redis_connection.dart'; import 'package:querya_desktop/core/database/redis_info.dart'; import 'package:querya_desktop/core/storage/folders_storage.dart'; @@ -16,6 +17,7 @@ class ConnectionsPanel extends StatefulWidget { super.key, this.onConnectionSelected, this.onRedisDatabaseSelected, + this.onMongoDBDatabaseSelected, }); /// Called when the user taps a connection tile. @@ -25,6 +27,10 @@ class ConnectionsPanel extends StatefulWidget { final void Function(ConnectionRow connection, int database)? onRedisDatabaseSelected; + /// Called when the user taps a MongoDB database node in the tree. + final void Function(ConnectionRow connection, String database)? + onMongoDBDatabaseSelected; + @override State createState() => _ConnectionsPanelState(); } @@ -194,6 +200,7 @@ class _ConnectionsPanelState extends State { onRemoveConnection: _removeConnection, onConnectionTap: widget.onConnectionSelected, onRedisDatabaseTap: widget.onRedisDatabaseSelected, + onMongoDBDatabaseTap: widget.onMongoDBDatabaseSelected, ), // Root connections (no folder) for (final conn in rootConnections) @@ -206,13 +213,22 @@ class _ConnectionsPanelState extends State { onTap: () => widget.onConnectionSelected?.call(conn), onDatabaseTap: (db) => widget.onRedisDatabaseSelected?.call(conn, db), ) - : _ConnectionTile( - connection: conn, - icon: _iconForType(conn.type), - iconAsset: _iconAssetForType(conn.type), - onRemove: () => _removeConnection(conn.id!), - onTap: () => widget.onConnectionSelected?.call(conn), - ), + : conn.type == 'mongodb' + ? _MongoConnectionTile( + connection: conn, + icon: _iconForType(conn.type), + iconAsset: _iconAssetForType(conn.type), + onRemove: () => _removeConnection(conn.id!), + onTap: () => widget.onConnectionSelected?.call(conn), + onDatabaseTap: (db) => widget.onMongoDBDatabaseSelected?.call(conn, db), + ) + : _ConnectionTile( + connection: conn, + icon: _iconForType(conn.type), + iconAsset: _iconAssetForType(conn.type), + onRemove: () => _removeConnection(conn.id!), + onTap: () => widget.onConnectionSelected?.call(conn), + ), // Empty state if (_connections.isEmpty && _folders.isEmpty) const material.Padding( @@ -403,6 +419,7 @@ class _FolderTile extends StatelessWidget { required this.onRemoveConnection, this.onConnectionTap, this.onRedisDatabaseTap, + this.onMongoDBDatabaseTap, }); final String name; @@ -415,6 +432,7 @@ class _FolderTile extends StatelessWidget { final Future Function(int id) onRemoveConnection; final void Function(ConnectionRow connection)? onConnectionTap; final void Function(ConnectionRow connection, int database)? onRedisDatabaseTap; + final void Function(ConnectionRow connection, String database)? onMongoDBDatabaseTap; @override Widget build(BuildContext context) { @@ -489,13 +507,22 @@ class _FolderTile extends StatelessWidget { onTap: () => onConnectionTap?.call(conn), onDatabaseTap: (db) => onRedisDatabaseTap?.call(conn, db), ) - : _ConnectionTile( - connection: conn, - icon: iconForType(conn.type), - iconAsset: _ConnectionsPanelState._iconAssetForType(conn.type), - onRemove: () => onRemoveConnection(conn.id!), - onTap: () => onConnectionTap?.call(conn), - ), + : conn.type == 'mongodb' + ? _MongoConnectionTile( + connection: conn, + icon: iconForType(conn.type), + iconAsset: _ConnectionsPanelState._iconAssetForType(conn.type), + onRemove: () => onRemoveConnection(conn.id!), + onTap: () => onConnectionTap?.call(conn), + onDatabaseTap: (db) => onMongoDBDatabaseTap?.call(conn, db), + ) + : _ConnectionTile( + connection: conn, + icon: iconForType(conn.type), + iconAsset: _ConnectionsPanelState._iconAssetForType(conn.type), + onRemove: () => onRemoveConnection(conn.id!), + onTap: () => onConnectionTap?.call(conn), + ), ), ], ), @@ -814,3 +841,282 @@ class _RedisDatabaseNode extends StatelessWidget { ); } } + +// ─── MongoDB connection tile with expandable database tree ────────────────── + +class _MongoConnectionTile extends StatefulWidget { + const _MongoConnectionTile({ + required this.connection, + required this.icon, + this.iconAsset, + required this.onRemove, + this.onTap, + this.onDatabaseTap, + }); + + final ConnectionRow connection; + final material.IconData icon; + final String? iconAsset; + final VoidCallback onRemove; + final VoidCallback? onTap; + final void Function(String database)? onDatabaseTap; + + @override + State<_MongoConnectionTile> createState() => _MongoConnectionTileState(); +} + +class _MongoConnectionTileState extends State<_MongoConnectionTile> { + bool _expanded = false; + bool _loading = false; + String? _error; + List _databases = []; + + void _toggle() { + setState(() => _expanded = !_expanded); + if (_expanded && _databases.isEmpty && !_loading) { + _loadDatabases(); + } + } + + Future _loadDatabases() async { + if (!mounted) return; + setState(() { + _loading = true; + _error = null; + }); + try { + final c = widget.connection; + final conn = MongoConnection( + id: -1, + name: 'sidebar_probe', + host: c.host ?? 'localhost', + port: c.port ?? 27017, + username: c.username, + password: c.password, + database: c.databaseName, + authSource: c.authSource, + useSSL: c.useSSL, + connectionString: c.connectionString, + ); + await conn.connect(); + final dbs = await conn.listDatabases(); + await conn.disconnect(); + + if (!mounted) return; + setState(() { + _databases = dbs; + _loading = false; + }); + } catch (e) { + if (!mounted) return; + setState(() { + _error = e.toString(); + _loading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final iconWidget = widget.iconAsset != null + ? material.Image.asset( + widget.iconAsset!, + width: 16, + height: 16, + fit: material.BoxFit.contain, + errorBuilder: (_, __, ___) => material.Icon( + widget.icon, + size: 16, + color: theme.colorScheme.primary, + ), + ) + : material.Icon(widget.icon, size: 16, color: theme.colorScheme.primary); + + return ContextMenu( + items: [ + MenuButton( + leading: material.Icon(material.Icons.refresh_rounded, + size: 18, color: theme.colorScheme.mutedForeground), + onPressed: (_) { + _databases = []; + _loadDatabases(); + }, + child: const Text('Refresh databases'), + ), + MenuButton( + leading: material.Icon(material.Icons.delete_outline_rounded, + size: 18, color: theme.colorScheme.mutedForeground), + onPressed: (_) => widget.onRemove(), + child: const Text('Remove connection'), + ), + ], + child: material.Padding( + padding: const material.EdgeInsets.only(bottom: 2), + child: material.Column( + crossAxisAlignment: material.CrossAxisAlignment.start, + mainAxisSize: material.MainAxisSize.min, + children: [ + // Connection row + material.Row( + children: [ + // Expand/collapse arrow + material.MouseRegion( + cursor: material.SystemMouseCursors.click, + child: material.InkWell( + onTap: _toggle, + borderRadius: material.BorderRadius.circular(4), + child: material.Padding( + padding: const material.EdgeInsets.all(2), + child: material.AnimatedRotation( + turns: _expanded ? 0.25 : 0, + duration: const Duration(milliseconds: 150), + child: material.Icon( + material.Icons.chevron_right_rounded, + size: 16, + color: theme.colorScheme.mutedForeground, + ), + ), + ), + ), + ), + // Connection name — clickable for stats + material.Expanded( + child: material.MouseRegion( + cursor: material.SystemMouseCursors.click, + child: material.InkWell( + onTap: widget.onTap, + borderRadius: material.BorderRadius.circular(6), + child: material.Padding( + padding: const material.EdgeInsets.symmetric( + horizontal: 4, vertical: 6), + child: material.Row( + children: [ + iconWidget, + const Gap(8), + material.Expanded( + child: material.Column( + crossAxisAlignment: + material.CrossAxisAlignment.start, + mainAxisSize: material.MainAxisSize.min, + children: [ + material.Text( + widget.connection.name, + overflow: material.TextOverflow.ellipsis, + maxLines: 1, + style: material.TextStyle( + fontSize: 13, + color: theme.colorScheme.foreground, + ), + ), + if (widget.connection.host != null) + material.Text( + '${widget.connection.host}:${widget.connection.port ?? ''}', + overflow: material.TextOverflow.ellipsis, + maxLines: 1, + style: material.TextStyle( + fontSize: 11, + color: theme.colorScheme.mutedForeground, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ), + ], + ), + // Expanded database children + if (_expanded) ...[ + if (_loading) + material.Padding( + padding: const material.EdgeInsets.only(left: 28, top: 4, bottom: 4), + child: material.Row( + children: [ + const material.SizedBox( + width: 12, + height: 12, + child: material.CircularProgressIndicator(strokeWidth: 1.5), + ), + const Gap(8), + const Text('Loading...').muted().xSmall(), + ], + ), + ), + if (_error != null) + material.Padding( + padding: const material.EdgeInsets.only(left: 28, top: 4, bottom: 4), + child: material.Text( + 'Error', + overflow: material.TextOverflow.ellipsis, + maxLines: 1, + style: material.TextStyle( + fontSize: 11, color: theme.colorScheme.destructive), + ), + ), + for (final db in _databases) + _MongoDatabaseNode( + name: db, + onTap: () => widget.onDatabaseTap?.call(db), + ), + ], + ], + ), + ), + ); + } +} + +class _MongoDatabaseNode extends StatelessWidget { + const _MongoDatabaseNode({ + required this.name, + required this.onTap, + }); + + final String name; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return material.Padding( + padding: const material.EdgeInsets.only(left: 24), + child: material.MouseRegion( + cursor: material.SystemMouseCursors.click, + child: material.InkWell( + onTap: onTap, + borderRadius: material.BorderRadius.circular(6), + child: material.Padding( + padding: const material.EdgeInsets.symmetric( + horizontal: 8, vertical: 5), + child: material.Row( + children: [ + material.Icon( + material.Icons.storage_rounded, + size: 14, + color: theme.colorScheme.primary.withValues(alpha: 0.7), + ), + const Gap(8), + material.Expanded( + child: material.Text( + name, + overflow: material.TextOverflow.ellipsis, + maxLines: 1, + style: material.TextStyle( + fontSize: 12, + color: theme.colorScheme.foreground, + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/features/main_screen/main_screen.dart b/lib/features/main_screen/main_screen.dart index 43272e96..661de173 100644 --- a/lib/features/main_screen/main_screen.dart +++ b/lib/features/main_screen/main_screen.dart @@ -27,10 +27,14 @@ class _MainScreenState extends State { /// Currently selected Redis database (null = show stats). int? _activeRedisDb; + /// Currently selected MongoDB database (null = show stats). + String? _activeMongoDB; + void _onConnectionSelected(ConnectionRow connection) { setState(() { _activeConnection = connection; _activeRedisDb = null; // Reset: connection click → stats + _activeMongoDB = null; }); } @@ -38,6 +42,15 @@ class _MainScreenState extends State { setState(() { _activeConnection = connection; _activeRedisDb = database; + _activeMongoDB = null; + }); + } + + void _onMongoDBDatabaseSelected(ConnectionRow connection, String database) { + setState(() { + _activeConnection = connection; + _activeRedisDb = null; + _activeMongoDB = database; }); } @@ -63,6 +76,7 @@ class _MainScreenState extends State { child: ConnectionsPanel( onConnectionSelected: _onConnectionSelected, onRedisDatabaseSelected: _onRedisDatabaseSelected, + onMongoDBDatabaseSelected: _onMongoDBDatabaseSelected, ), ), _VerticalResizeHandle( @@ -77,6 +91,7 @@ class _MainScreenState extends State { child: WorkspacePanel( activeConnection: _activeConnection, selectedRedisDb: _activeRedisDb, + selectedMongoDb: _activeMongoDB, ), ), ], diff --git a/lib/features/main_screen/workspace_panel.dart b/lib/features/main_screen/workspace_panel.dart index a375997c..5021557b 100644 --- a/lib/features/main_screen/workspace_panel.dart +++ b/lib/features/main_screen/workspace_panel.dart @@ -14,6 +14,7 @@ class WorkspacePanel extends StatefulWidget { super.key, this.activeConnection, this.selectedRedisDb, + this.selectedMongoDb, }); /// Currently selected connection from the sidebar. @@ -23,6 +24,10 @@ class WorkspacePanel extends StatefulWidget { /// null = show stats, non-null = show data explorer for that db. final int? selectedRedisDb; + /// When set, the user selected a specific MongoDB database in the sidebar tree. + /// null = show stats, non-null = show data explorer for that db. + final String? selectedMongoDb; + @override State createState() => _WorkspacePanelState(); } @@ -36,16 +41,25 @@ class _WorkspacePanelState extends State { Widget build(BuildContext context) { final theme = Theme.of(context); - // If a MongoDB connection is selected, show the MongoDB explorer + // If a MongoDB connection is selected if (widget.activeConnection != null && widget.activeConnection!.type == 'mongodb') { + final mongoDb = widget.selectedMongoDb; return material.Container( color: theme.colorScheme.background, child: material.SizedBox.expand( - child: MongoExplorerView( - key: ValueKey(widget.activeConnection!.id), - connectionRow: widget.activeConnection!, - ), + // DB selected → data explorer; no DB → stats + child: mongoDb != null + ? MongoExplorerView( + key: ValueKey( + 'mongo_${widget.activeConnection!.id}_db_$mongoDb'), + connectionRow: widget.activeConnection!, + database: mongoDb, + ) + : MongoExplorerView( + key: ValueKey(widget.activeConnection!.id), + connectionRow: widget.activeConnection!, + ), ), ); } diff --git a/lib/features/mongodb/mongo_explorer_view.dart b/lib/features/mongodb/mongo_explorer_view.dart index abc3ddc6..efe1798a 100644 --- a/lib/features/mongodb/mongo_explorer_view.dart +++ b/lib/features/mongodb/mongo_explorer_view.dart @@ -27,8 +27,13 @@ enum _Level { databases, collections, documents, document } /// Root widget for MongoDB data browsing. /// Manages navigation state (breadcrumbs) and the active connection. class MongoExplorerView extends material.StatefulWidget { - const MongoExplorerView({super.key, required this.connectionRow}); + const MongoExplorerView({ + super.key, + required this.connectionRow, + this.database, + }); final ConnectionRow connectionRow; + final String? database; @override material.State createState() => _MongoExplorerViewState(); @@ -51,6 +56,9 @@ class _MongoExplorerViewState extends material.State { void initState() { super.initState(); _connect(); + if (widget.database != null) { + _selectedDatabase = widget.database; + } } @override From 90013a9918bf10dcfc759e19ed3cefb4709f8af7 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Tue, 10 Mar 2026 06:10:02 +0300 Subject: [PATCH 3/7] Add MongoDB stats view and database management (create/delete) Made-with: Cursor --- lib/core/database/mongodb_connection.dart | 20 +++ .../connections/connections_panel.dart | 122 ++++++++++++++---- lib/features/main_screen/workspace_panel.dart | 3 +- .../mongodb/mongo_database_dialog.dart | 122 ++++++++++++++++++ 4 files changed, 238 insertions(+), 29 deletions(-) create mode 100644 lib/features/mongodb/mongo_database_dialog.dart diff --git a/lib/core/database/mongodb_connection.dart b/lib/core/database/mongodb_connection.dart index 1fafdbe7..916e8de4 100644 --- a/lib/core/database/mongodb_connection.dart +++ b/lib/core/database/mongodb_connection.dart @@ -212,4 +212,24 @@ class MongoConnection { return false; } } + + /// Drops (deletes) a database. + Future dropDatabase(String databaseName) async { + if (!isConnected || _db == null) { + throw StateError('Not connected to MongoDB'); + } + + try { + final dbUri = buildUriForDatabase(databaseName); + final db = await Db.create(dbUri); + await db.open(); + try { + await db.drop(); + } finally { + await db.close(); + } + } catch (e) { + rethrow; + } + } } diff --git a/lib/features/connections/connections_panel.dart b/lib/features/connections/connections_panel.dart index 9f4b7bb4..03068f50 100644 --- a/lib/features/connections/connections_panel.dart +++ b/lib/features/connections/connections_panel.dart @@ -6,6 +6,7 @@ import 'package:querya_desktop/core/storage/folders_storage.dart'; import 'package:querya_desktop/core/storage/local_db.dart'; import 'package:querya_desktop/shared/widgets/widgets.dart'; +import 'package:querya_desktop/features/mongodb/mongo_database_dialog.dart'; import 'package:querya_desktop/features/mongodb/mongodb_connection_form.dart'; import 'package:querya_desktop/features/redis/redis_connection_form.dart'; import 'new_connection_dialog.dart'; @@ -642,6 +643,12 @@ class _RedisConnectionTileState extends State<_RedisConnectionTile> { return ContextMenu( items: [ + MenuButton( + leading: material.Icon(material.Icons.add_rounded, + size: 18, color: theme.colorScheme.mutedForeground), + onPressed: (_) => _createDatabase(), + child: const Text('Create database'), + ), MenuButton( leading: material.Icon(material.Icons.refresh_rounded, size: 18, color: theme.colorScheme.mutedForeground), @@ -916,6 +923,46 @@ class _MongoConnectionTileState extends State<_MongoConnectionTile> { } } + Future _createDatabase() async { + final dbName = await showCreateMongoDBDialog(context); + if (dbName == null || !mounted) return; + _databases = []; + await _loadDatabases(); + } + + Future _deleteDatabase(String dbName) async { + if (!mounted) return; + try { + final c = widget.connection; + final conn = MongoConnection( + id: -1, + name: 'sidebar_probe', + host: c.host ?? 'localhost', + port: c.port ?? 27017, + username: c.username, + password: c.password, + database: c.databaseName, + authSource: c.authSource, + useSSL: c.useSSL, + connectionString: c.connectionString, + ); + await conn.connect(); + await conn.dropDatabase(dbName); + await conn.disconnect(); + + if (mounted) { + _databases = []; + await _loadDatabases(); + } + } catch (e) { + if (mounted) { + setState(() { + _error = e.toString(); + }); + } + } + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -935,6 +982,12 @@ class _MongoConnectionTileState extends State<_MongoConnectionTile> { return ContextMenu( items: [ + MenuButton( + leading: material.Icon(material.Icons.add_rounded, + size: 18, color: theme.colorScheme.mutedForeground), + onPressed: (_) => _createDatabase(), + child: const Text('Create database'), + ), MenuButton( leading: material.Icon(material.Icons.refresh_rounded, size: 18, color: theme.colorScheme.mutedForeground), @@ -1062,6 +1115,7 @@ class _MongoConnectionTileState extends State<_MongoConnectionTile> { _MongoDatabaseNode( name: db, onTap: () => widget.onDatabaseTap?.call(db), + onDelete: () => _deleteDatabase(db), ), ], ], @@ -1075,44 +1129,56 @@ class _MongoDatabaseNode extends StatelessWidget { const _MongoDatabaseNode({ required this.name, required this.onTap, + required this.onDelete, }); final String name; final VoidCallback onTap; + final VoidCallback onDelete; @override Widget build(BuildContext context) { final theme = Theme.of(context); - return material.Padding( - padding: const material.EdgeInsets.only(left: 24), - child: material.MouseRegion( - cursor: material.SystemMouseCursors.click, - child: material.InkWell( - onTap: onTap, - borderRadius: material.BorderRadius.circular(6), - child: material.Padding( - padding: const material.EdgeInsets.symmetric( - horizontal: 8, vertical: 5), - child: material.Row( - children: [ - material.Icon( - material.Icons.storage_rounded, - size: 14, - color: theme.colorScheme.primary.withValues(alpha: 0.7), - ), - const Gap(8), - material.Expanded( - child: material.Text( - name, - overflow: material.TextOverflow.ellipsis, - maxLines: 1, - style: material.TextStyle( - fontSize: 12, - color: theme.colorScheme.foreground, + return ContextMenu( + items: [ + MenuButton( + leading: material.Icon(material.Icons.delete_outline_rounded, + size: 18, color: theme.colorScheme.mutedForeground), + onPressed: (_) => onDelete(), + child: const Text('Delete database'), + ), + ], + child: material.Padding( + padding: const material.EdgeInsets.only(left: 24), + child: material.MouseRegion( + cursor: material.SystemMouseCursors.click, + child: material.InkWell( + onTap: onTap, + borderRadius: material.BorderRadius.circular(6), + child: material.Padding( + padding: const material.EdgeInsets.symmetric( + horizontal: 8, vertical: 5), + child: material.Row( + children: [ + material.Icon( + material.Icons.storage_rounded, + size: 14, + color: theme.colorScheme.primary.withValues(alpha: 0.7), + ), + const Gap(8), + material.Expanded( + child: material.Text( + name, + overflow: material.TextOverflow.ellipsis, + maxLines: 1, + style: material.TextStyle( + fontSize: 12, + color: theme.colorScheme.foreground, + ), ), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/features/main_screen/workspace_panel.dart b/lib/features/main_screen/workspace_panel.dart index 5021557b..cfddbe6a 100644 --- a/lib/features/main_screen/workspace_panel.dart +++ b/lib/features/main_screen/workspace_panel.dart @@ -3,6 +3,7 @@ import 'package:querya_desktop/core/storage/local_db.dart'; import 'package:querya_desktop/shared/widgets/widgets.dart'; import 'package:querya_desktop/features/mongodb/mongo_explorer_view.dart'; +import 'package:querya_desktop/features/mongodb/mongo_stats_view.dart'; import 'package:querya_desktop/features/redis/redis_explorer_view.dart'; import 'package:querya_desktop/features/redis/redis_view.dart'; import 'query_editor_tab.dart'; @@ -56,7 +57,7 @@ class _WorkspacePanelState extends State { connectionRow: widget.activeConnection!, database: mongoDb, ) - : MongoExplorerView( + : MongoStatsView( key: ValueKey(widget.activeConnection!.id), connectionRow: widget.activeConnection!, ), diff --git a/lib/features/mongodb/mongo_database_dialog.dart b/lib/features/mongodb/mongo_database_dialog.dart new file mode 100644 index 00000000..09edd630 --- /dev/null +++ b/lib/features/mongodb/mongo_database_dialog.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart' as material; +import 'package:querya_desktop/shared/widgets/widgets.dart'; + +/// Shows dialog to create a new MongoDB database. +Future showCreateMongoDBDialog(material.BuildContext context) async { + return showDialog( + context: context, + barrierColor: material.Colors.black54, + builder: (context) => material.Dialog( + backgroundColor: material.Colors.transparent, + insetPadding: const material.EdgeInsets.symmetric(horizontal: 40, vertical: 24), + child: const _CreateMongoDBDialogContent(), + ), + ); +} + +class _CreateMongoDBDialogContent extends material.StatefulWidget { + const _CreateMongoDBDialogContent(); + + @override + material.State<_CreateMongoDBDialogContent> createState() => _CreateMongoDBDialogContentState(); +} + +class _CreateMongoDBDialogContentState extends material.State<_CreateMongoDBDialogContent> { + final _nameController = material.TextEditingController(); + + @override + void initState() { + super.initState(); + _nameController.addListener(_onFieldChanged); + } + + void _onFieldChanged() => setState(() {}); + + bool get _formValid => _nameController.text.trim().isNotEmpty; + + void _save() { + if (!_formValid) return; + material.Navigator.of(context).pop(_nameController.text.trim()); + } + + @override + void dispose() { + _nameController.removeListener(_onFieldChanged); + _nameController.dispose(); + super.dispose(); + } + + @override + material.Widget build(material.BuildContext context) { + final theme = Theme.of(context).colorScheme; + final radius = Theme.of(context).radiusXxl; + + return material.Container( + constraints: const material.BoxConstraints(maxWidth: 500), + decoration: material.BoxDecoration( + color: theme.popover, + borderRadius: material.BorderRadius.circular(radius), + border: material.Border.all(color: theme.muted), + ), + child: material.ClipRRect( + borderRadius: material.BorderRadius.circular(radius), + child: material.Column( + mainAxisSize: material.MainAxisSize.min, + crossAxisAlignment: material.CrossAxisAlignment.stretch, + children: [ + material.Padding( + padding: const material.EdgeInsets.fromLTRB(24, 24, 24, 16), + child: material.Column( + crossAxisAlignment: material.CrossAxisAlignment.stretch, + children: [ + material.Row( + children: [ + material.Icon(material.Icons.storage_rounded, size: 24, color: theme.primary), + const Gap(12), + const Text('Create Database').large().semiBold(), + ], + ), + const Gap(8), + const Text('Enter the name for the new MongoDB database.').muted().small(), + ], + ), + ), + const material.Divider(height: 1), + material.Padding( + padding: const material.EdgeInsets.all(24), + child: material.Column( + crossAxisAlignment: material.CrossAxisAlignment.stretch, + children: [ + const Text('Database Name').small().semiBold(), + const Gap(8), + TextField( + controller: _nameController, + placeholder: const Text('mydb'), + ), + ], + ), + ), + const material.Divider(height: 1), + material.Container( + padding: const material.EdgeInsets.symmetric(horizontal: 24, vertical: 16), + child: material.Row( + mainAxisAlignment: material.MainAxisAlignment.end, + children: [ + GhostButton( + onPressed: () => material.Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + const Gap(12), + PrimaryButton( + onPressed: _formValid ? _save : null, + child: const Text('Create'), + ), + ], + ), + ), + ], + ), + ), + ); + } +} From 5fd3c8a368c5a364b6e79d60f74d1d61be95dfd8 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Tue, 10 Mar 2026 06:11:02 +0300 Subject: [PATCH 4/7] Fix: Remove 'Create database' from Redis context menu (MongoDB only) Made-with: Cursor --- lib/features/connections/connections_panel.dart | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/features/connections/connections_panel.dart b/lib/features/connections/connections_panel.dart index 03068f50..a25f025b 100644 --- a/lib/features/connections/connections_panel.dart +++ b/lib/features/connections/connections_panel.dart @@ -643,12 +643,6 @@ class _RedisConnectionTileState extends State<_RedisConnectionTile> { return ContextMenu( items: [ - MenuButton( - leading: material.Icon(material.Icons.add_rounded, - size: 18, color: theme.colorScheme.mutedForeground), - onPressed: (_) => _createDatabase(), - child: const Text('Create database'), - ), MenuButton( leading: material.Icon(material.Icons.refresh_rounded, size: 18, color: theme.colorScheme.mutedForeground), From 044468dc743bc4c4f6b5b98b22a5b725dba32d23 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Tue, 10 Mar 2026 06:20:12 +0300 Subject: [PATCH 5/7] Fix: prefer_const_constructors in mongo_database_dialog Made-with: Cursor --- lib/features/mongodb/mongo_database_dialog.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/features/mongodb/mongo_database_dialog.dart b/lib/features/mongodb/mongo_database_dialog.dart index 09edd630..f9bb7c05 100644 --- a/lib/features/mongodb/mongo_database_dialog.dart +++ b/lib/features/mongodb/mongo_database_dialog.dart @@ -6,10 +6,10 @@ Future showCreateMongoDBDialog(material.BuildContext context) async { return showDialog( context: context, barrierColor: material.Colors.black54, - builder: (context) => material.Dialog( + builder: (context) => const material.Dialog( backgroundColor: material.Colors.transparent, - insetPadding: const material.EdgeInsets.symmetric(horizontal: 40, vertical: 24), - child: const _CreateMongoDBDialogContent(), + insetPadding: material.EdgeInsets.symmetric(horizontal: 40, vertical: 24), + child: _CreateMongoDBDialogContent(), ), ); } From 32b29e3d9ddd11b9133710d327c7d6a27b9663a5 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Tue, 10 Mar 2026 06:20:13 +0300 Subject: [PATCH 6/7] Test: add dropDatabase unit test for MongoConnection Made-with: Cursor --- test/core/database/mongodb_connection_test.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/core/database/mongodb_connection_test.dart b/test/core/database/mongodb_connection_test.dart index 32f72a65..51a4254a 100644 --- a/test/core/database/mongodb_connection_test.dart +++ b/test/core/database/mongodb_connection_test.dart @@ -181,4 +181,18 @@ void main() { expect(conn.db, isNull); }); }); + + group('MongoConnection.dropDatabase', () { + test('throws StateError when not connected', () async { + final conn = MongoConnection(id: 1, name: 'test', host: 'localhost'); + expect( + () => conn.dropDatabase('mydb'), + throwsA(isA().having( + (e) => e.message, + 'message', + contains('Not connected'), + )), + ); + }); + }); } From 607aff78025c1dbf6f3d34352218264f347d7553 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Tue, 10 Mar 2026 06:20:14 +0300 Subject: [PATCH 7/7] Test: add widget tests for Create MongoDB database dialog Made-with: Cursor --- .../mongodb/mongo_database_dialog_test.dart | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 test/features/mongodb/mongo_database_dialog_test.dart diff --git a/test/features/mongodb/mongo_database_dialog_test.dart b/test/features/mongodb/mongo_database_dialog_test.dart new file mode 100644 index 00000000..e32eb440 --- /dev/null +++ b/test/features/mongodb/mongo_database_dialog_test.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart' as material; +import 'package:flutter_test/flutter_test.dart'; +import 'package:querya_desktop/core/theme/app_theme.dart'; +import 'package:querya_desktop/features/mongodb/mongo_database_dialog.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +void main() { + group('showCreateMongoDBDialog', () { + testWidgets('dialog shows Create Database title and form', (tester) async { + await tester.pumpWidget( + ShadcnApp( + theme: AppTheme.dark, + darkTheme: AppTheme.dark, + themeMode: ThemeMode.dark, + home: material.Builder( + builder: (context) => material.ElevatedButton( + onPressed: () => showCreateMongoDBDialog(context), + child: const material.Text('Open'), + ), + ), + ), + ); + + await tester.tap(find.text('Open')); + await tester.pumpAndSettle(); + + expect(find.text('Create Database'), findsOneWidget); + expect(find.text('Database Name'), findsOneWidget); + expect(find.text('Create'), findsOneWidget); + expect(find.text('Cancel'), findsOneWidget); + }); + + testWidgets('Cancel closes dialog and returns null', (tester) async { + String? result; + await tester.pumpWidget( + ShadcnApp( + theme: AppTheme.dark, + darkTheme: AppTheme.dark, + themeMode: ThemeMode.dark, + home: material.Builder( + builder: (context) => material.ElevatedButton( + onPressed: () async { + result = await showCreateMongoDBDialog(context); + }, + child: const material.Text('Open'), + ), + ), + ), + ); + + await tester.tap(find.text('Open')); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Cancel')); + await tester.pumpAndSettle(); + + expect(result, isNull); + }); + }); +}