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
161 changes: 95 additions & 66 deletions lib/features/mongodb/mongo_documents_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,7 @@ class _DocumentCardState extends State<_DocumentCard> {
padding: const material.EdgeInsets.only(
left: 16, right: 16, bottom: 10),
child: _expanded
? _FieldList(
? MongoFieldList(
document: widget.document,
colorScheme: cs,
shadcnCs: scs,
Expand Down Expand Up @@ -584,8 +584,10 @@ class _DocumentCardState extends State<_DocumentCard> {
}
}

class _FieldList extends StatelessWidget {
const _FieldList({
@material.visibleForTesting
class MongoFieldList extends StatelessWidget {
const MongoFieldList({
super.key,
required this.document,
required this.colorScheme,
required this.shadcnCs,
Expand All @@ -597,77 +599,104 @@ class _FieldList extends StatelessWidget {
final shadcn.ColorScheme shadcnCs;
final ValueChanged<String> onInspectField;

Widget _buildFieldRow(String key) {
final canEdit = key != '_id';
final display = mongoFieldToDisplay(document[key]);
final oneLine = display.replaceAll('\n', ' ');
return material.InkWell(
onTap: canEdit ? () => onInspectField(key) : null,
child: material.Padding(
padding: const material.EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
child: Row(
children: [
material.SizedBox(
width: 120,
child: Text(
key,
overflow: TextOverflow.ellipsis,
style: material.TextStyle(
fontSize: 12,
fontFamily: 'monospace',
fontWeight: material.FontWeight.w600,
color: colorScheme.foreground,
),
),
),
const Gap(8),
material.Expanded(
child: Text(
oneLine,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: material.TextStyle(
fontSize: 12,
fontFamily: 'monospace',
color: shadcnCs.mutedForeground,
),
),
),
if (canEdit)
material.Icon(
material.Icons.edit_note_rounded,
size: 16,
color: shadcnCs.mutedForeground,
),
],
),
),
);
}

Widget _buildDivider() => material.Divider(
height: 1,
color: colorScheme.border.withValues(alpha: 0.2),
);

@override
Widget build(BuildContext context) {
final fields = document.keys.toList();
if (fields.isEmpty) return const material.SizedBox.shrink();

final decoration = material.BoxDecoration(
color: colorScheme.muted.withValues(alpha: 0.15),
borderRadius: material.BorderRadius.circular(6),
border: material.Border.all(
color: colorScheme.border.withValues(alpha: 0.3),
),
);

// For documents with <= 10 fields, render a lightweight Column with zero
// scroll machinery or shrinkWrap measurement passes.
if (fields.length <= 10) {
final children = <Widget>[];
for (var i = 0; i < fields.length; i++) {
if (i > 0) children.add(_buildDivider());
children.add(_buildFieldRow(fields[i]));
}
return material.Container(
decoration: decoration,
padding: const material.EdgeInsets.symmetric(vertical: 4),
child: material.Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: material.CrossAxisAlignment.stretch,
children: children,
),
);
}

// For wide documents (> 10 fields), render a bounded virtualized list
// without shrinkWrap to avoid layout recalculation spikes on scroll.
return material.Container(
constraints: const material.BoxConstraints(maxHeight: 320),
decoration: material.BoxDecoration(
color: colorScheme.muted.withValues(alpha: 0.15),
borderRadius: material.BorderRadius.circular(6),
border: material.Border.all(
color: colorScheme.border.withValues(alpha: 0.3),
),
),
decoration: decoration,
child: ListView.separated(
shrinkWrap: true,
padding: const material.EdgeInsets.symmetric(vertical: 4),
itemCount: fields.length,
separatorBuilder: (_, __) => material.Divider(
height: 1,
color: colorScheme.border.withValues(alpha: 0.2),
),
itemBuilder: (context, i) {
final key = fields[i];
final canEdit = key != '_id';
final display = mongoFieldToDisplay(document[key]);
final oneLine = display.replaceAll('\n', ' ');
return material.InkWell(
onTap: canEdit ? () => onInspectField(key) : null,
child: material.Padding(
padding: const material.EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
child: Row(
children: [
material.SizedBox(
width: 120,
child: Text(
key,
overflow: TextOverflow.ellipsis,
style: material.TextStyle(
fontSize: 12,
fontFamily: 'monospace',
fontWeight: material.FontWeight.w600,
color: colorScheme.foreground,
),
),
),
const Gap(8),
material.Expanded(
child: Text(
oneLine,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: material.TextStyle(
fontSize: 12,
fontFamily: 'monospace',
color: shadcnCs.mutedForeground,
),
),
),
if (canEdit)
material.Icon(
material.Icons.edit_note_rounded,
size: 16,
color: shadcnCs.mutedForeground,
),
],
),
),
);
},
separatorBuilder: (_, __) => _buildDivider(),
itemBuilder: (context, i) => _buildFieldRow(fields[i]),
),
);
}
Expand Down
91 changes: 91 additions & 0 deletions test/features/mongodb/mongo_field_list_perf_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import 'package:flutter/material.dart' as material;
import 'package:flutter_test/flutter_test.dart';
import 'package:querya_desktop/features/mongodb/mongo_documents_view.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart' as shadcn;

import '../../support/querya_theme_test_shell.dart';

void main() {
group('MongoFieldList virtualization and layout optimization', () {
testWidgets('renders lightweight Column and no ListView when <= 10 fields',
(tester) async {
final doc = {
'_id': '64b1f2a3c9e77a1234567890',
'title': 'Test Item',
'count': 42,
'active': true,
};

String? inspectedKey;

await tester.pumpWidget(
queryaThemeTestShell(
child: material.Builder(
builder: (context) {
final scs = shadcn.Theme.of(context).colorScheme;
return material.Scaffold(
body: MongoFieldList(
document: doc,
colorScheme: scs,
shadcnCs: scs,
onInspectField: (k) => inspectedKey = k,
),
);
},
),
),
);
await tester.pumpAndSettle();

// Should render as Column to eliminate scrollable/shrinkWrap overhead
expect(find.byType(material.Column), findsOneWidget);
expect(find.byType(material.ListView), findsNothing);

// Verify fields are present
expect(find.text('title'), findsOneWidget);
expect(find.text('Test Item'), findsOneWidget);

// Inspecting field
await tester.tap(find.text('title'));
await tester.pumpAndSettle();
expect(inspectedKey, 'title');
});

testWidgets('renders virtualized ListView without shrinkWrap when > 10 fields',
(tester) async {
// 15 fields
final doc = <String, dynamic>{
'_id': '64b1f2a3c9e77a1234567890',
};
for (var i = 1; i <= 14; i++) {
doc['field_$i'] = 'value_$i';
}

await tester.pumpWidget(
queryaThemeTestShell(
child: material.Builder(
builder: (context) {
final scs = shadcn.Theme.of(context).colorScheme;
return material.Scaffold(
body: MongoFieldList(
document: doc,
colorScheme: scs,
shadcnCs: scs,
onInspectField: (_) {},
),
);
},
),
),
);
await tester.pumpAndSettle();

// Must render ListView without shrinkWrap
final listViewFinder = find.byType(material.ListView);
expect(listViewFinder, findsOneWidget);

final listView = tester.widget<material.ListView>(listViewFinder);
expect(listView.shrinkWrap, isFalse);
});
});
}
Loading