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
3 changes: 2 additions & 1 deletion lib/features/main_screen/querya_status_bar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,8 @@ class QueryaStatusBar extends material.StatelessWidget {
),
const Gap(3),
material.Text(
'$rowCount rows${columnCount != null ? ", $columnCount cols" : ""}',
'$rowCount ${rowCount == 1 ? 'row' : 'rows'}'
'${columnCount != null ? ", $columnCount ${columnCount == 1 ? 'col' : 'cols'}" : ""}',
style: material.TextStyle(
fontSize: 10,
fontFamily: QueryaTypography.mono,
Expand Down
88 changes: 48 additions & 40 deletions lib/features/workspace/generic_sql_workspace.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1345,50 +1345,58 @@ class GenericSqlWorkspaceState extends material.State<GenericSqlWorkspace> {
),
],
),
bottom: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (session.planRoot != null)
material.Padding(
padding: const material.EdgeInsets.fromLTRB(8, 6, 8, 6),
child: material.Align(
alignment: material.Alignment.centerLeft,
child: material.SizedBox(
width: 200,
child: QueryaTabStrip(
labels: const ['Plan', 'Text'],
selectedIndex: session.planAsTree ? 0 : 1,
onSelected: (i) => setState(() => session.planAsTree = i == 0),
bottom: material.Padding(
// The results sit in a card like the editor above them.
padding: const material.EdgeInsets.fromLTRB(12, 0, 12, 12),
child: SqlEditorChrome(
background: context.workbench.surface,
showFocus: false,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (session.planRoot != null)
material.Padding(
padding: const material.EdgeInsets.fromLTRB(8, 6, 8, 6),
child: material.Align(
alignment: material.Alignment.centerLeft,
child: material.SizedBox(
width: 200,
child: QueryaTabStrip(
labels: const ['Plan', 'Text'],
selectedIndex: session.planAsTree ? 0 : 1,
onSelected: (i) => setState(() => session.planAsTree = i == 0),
),
),
),
),
Expanded(
child: session.planRoot != null && session.planAsTree
? PlanTreeView(root: session.planRoot!)
: ResultsTab(
columns: session.columns,
rows: session.rows,
errorMessage: session.error,
isLoading: session.running,
affectedRows: session.affectedRows,
statusLine: session.statusLine,
elapsed: session.elapsed,
stagingBuffer: session.stagingBuffer,
columnDataTypes: session.resultGridColumnDataTypes,
onApplyChanges: widget.isReadOnly ||
!sqlResultGridSaveEnabled(
sql: session.lastExecutedSql,
resultColumns: session.columns,
primaryKeys: session.resultGridPrimaryKeys,
)
? null
: () => applyStagedChanges(session),
isSaving: session.savingChanges,
errorAction: widget.errorActionBuilder?.call(context, session),
),
),
),
Expanded(
child: session.planRoot != null && session.planAsTree
? PlanTreeView(root: session.planRoot!)
: ResultsTab(
columns: session.columns,
rows: session.rows,
errorMessage: session.error,
isLoading: session.running,
affectedRows: session.affectedRows,
statusLine: session.statusLine,
elapsed: session.elapsed,
stagingBuffer: session.stagingBuffer,
columnDataTypes: session.resultGridColumnDataTypes,
onApplyChanges: widget.isReadOnly ||
!sqlResultGridSaveEnabled(
sql: session.lastExecutedSql,
resultColumns: session.columns,
primaryKeys: session.resultGridPrimaryKeys,
)
? null
: () => applyStagedChanges(session),
isSaving: session.savingChanges,
errorAction: widget.errorActionBuilder?.call(context, session),
),
],
),
],
),
),
);
}
Expand Down
30 changes: 24 additions & 6 deletions lib/features/workspace/results_tab.dart
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,27 @@ class _ResultsTabState extends material.State<ResultsTab> {
super.dispose();
}

/// What the mode bar says about the rows, or null when there is nothing to
/// add. A plain row count is the status bar's (#1558); here stay the notes
/// it does not have: a truncated result, affected rows, a script's outcome,
/// and the count of a filtered grid (`3 of 12 rows`).
String? _barSummary(int shown, int total) {
final line = widget.statusLine;
if (_filterText.isNotEmpty) {
return line != null && !_plainRowCount.hasMatch(line)
? '$line · $shown of $total rows'
: '$shown of $total rows';
}
if (line != null) return _plainRowCount.hasMatch(line) ? null : line;
if (widget.affectedRows != null) {
return 'Rows affected: ${widget.affectedRows}';
}
return '$shown rows';
}

/// `12 row(s).`: the driver workspaces' plain count.
static final _plainRowCount = RegExp(r'^\d+ row\(s\)\.$');

@override
material.Widget build(material.BuildContext context) {
if (widget.isLoading) {
Expand Down Expand Up @@ -519,12 +540,9 @@ class _ResultsTabState extends material.State<ResultsTab> {
setState(() => _viewMode = ResultViewMode.values[i]),
),
const Gap(10),
Text(
widget.statusLine ??
(widget.affectedRows != null
? 'Rows affected: ${widget.affectedRows}'
: '${filteredRows.length}${_filterText.isNotEmpty ? ' of ${effectiveRows.length}' : ''} rows'),
).small().semiBold(),
if (_barSummary(filteredRows.length, effectiveRows.length)
case final summary?)
Text(summary).small().semiBold(),
if (widget.elapsed != null) ...[
const Gap(8),
QueryaBadge(
Expand Down
65 changes: 38 additions & 27 deletions lib/features/workspace/sql_editor_chrome.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,28 @@ import 'package:querya_desktop/core/theme/querya_theme_scope.dart';
import 'package:querya_desktop/core/theme/querya_workbench_theme.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart' hide TextExtension;

/// Outer chrome for SQL editors: border, surface, brand accent glow.
class SqlEditorChrome extends StatelessWidget {
const SqlEditorChrome({super.key, required this.child});
/// Outer chrome for SQL editors and the results under them: one rounded card
/// with a border.
class SqlEditorChrome extends StatefulWidget {
const SqlEditorChrome({
super.key,
required this.child,
this.background,
this.showFocus = true,
});

final Widget child;

/// The card's surface; the editor background by default.
final material.Color? background;

/// Whether the border takes the accent while something inside has the
/// keyboard. Off for a card with nothing to type in (the results).
final bool showFocus;

static const double outerRadius = 14;
static const double innerRadius = 10;

/// Accent glow strength; slightly softer on light themes.
static double chromeGlowAlpha(Brightness brightness) =>
brightness == Brightness.light ? 0.08 : 0.1;

static double inlineGlowAlpha(Brightness brightness) =>
brightness == Brightness.light ? 0.05 : 0.07;

Expand Down Expand Up @@ -70,38 +79,40 @@ class SqlEditorChrome extends StatelessWidget {
);
}

@override
State<SqlEditorChrome> createState() => _SqlEditorChromeState();
}

class _SqlEditorChromeState extends State<SqlEditorChrome> {
bool _focused = false;

@override
Widget build(BuildContext context) {
final editor = context.editorTheme;
final workbench = context.workbench;
final brightness = Theme.of(context).brightness;
final border = editor.widgetBorder ?? workbench.borderSubtle;
final glow = workbench.accent.withValues(
alpha: chromeGlowAlpha(brightness),
);
final surface = widget.background ?? editor.background;

return material.Container(
decoration: material.BoxDecoration(
borderRadius: material.BorderRadius.circular(outerRadius),
boxShadow: [
material.BoxShadow(
color: glow,
blurRadius: 28,
spreadRadius: 0,
offset: const material.Offset(0, 10),
),
],
),
// One framed card, the same for the editor and the results. No glow: the
// only accent is the border while the editor has the keyboard (#1558).
return Focus(
canRequestFocus: false,
skipTraversal: true,
onFocusChange: (v) {
if (widget.showFocus) setState(() => _focused = v);
},
child: material.Container(
decoration: material.BoxDecoration(
color: editor.background,
borderRadius: material.BorderRadius.circular(outerRadius),
color: surface,
borderRadius: material.BorderRadius.circular(SqlEditorChrome.outerRadius),
border: material.Border.all(
color: border.withValues(alpha: 0.5),
color: widget.showFocus && _focused
? workbench.accent.withValues(alpha: 0.6)
: border.withValues(alpha: 0.5),
),
),
clipBehavior: material.Clip.antiAlias,
child: child,
child: widget.child,
),
);
}
Expand Down
14 changes: 14 additions & 0 deletions test/features/main_screen/querya_status_bar_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,20 @@ void main() {
expect(find.text('UTF-8'), findsNothing);
});

testWidgets('the row and column counts say one, not 1 rows (#1558)',
(tester) async {
await tester.pumpWidget(
queryaThemeTestShell(
child: const material.Scaffold(
body: QueryaStatusBar(rowCount: 1, columnCount: 1),
),
),
);
await tester.pump();

expect(find.text('1 row, 1 col'), findsOneWidget);
});

testWidgets('QueryaStatusBar renders active connection and read-only mode',
(tester) async {
final conn = ConnectionRow(
Expand Down
57 changes: 57 additions & 0 deletions test/features/workspace/results_tab_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,63 @@ void main() {
expect(find.text('3'), findsOneWidget);
});

group('the mode bar says only what the status bar does not (#1558)', () {
Future<void> pumpBar(
WidgetTester tester, {
String? statusLine,
int? affectedRows,
}) async {
await tester.binding.setSurfaceSize(const material.Size(900, 500));
addTearDown(() => tester.binding.setSurfaceSize(null));
await tester.pumpWidget(
resultsShell(
child: material.Scaffold(
body: ResultsTab(
columns: const ['id', 'name'],
rows: const [
['1', 'a'],
['2', 'b'],
['3', 'c'],
],
statusLine: statusLine,
affectedRows: affectedRows,
),
),
),
);
await tester.pump();
}

testWidgets('a plain row count is left to the status bar', (tester) async {
await pumpBar(tester, statusLine: '3 row(s).');
expect(find.text('3 row(s).'), findsNothing);
expect(find.text('3 rows'), findsNothing);
});

testWidgets('a truncation note is kept', (tester) async {
await pumpBar(
tester, statusLine: 'Showing first 3 row(s) (result capped).');
expect(find.text('Showing first 3 row(s) (result capped).'),
findsOneWidget);
});

testWidgets('a count with a hint is kept', (tester) async {
await pumpBar(tester, statusLine: '3 row(s). Cannot edit: no key.');
expect(find.text('3 row(s). Cannot edit: no key.'), findsOneWidget);
});

testWidgets('without a status line the count is shown, as before',
(tester) async {
await pumpBar(tester);
expect(find.text('3 rows'), findsOneWidget);
});

testWidgets('affected rows are kept', (tester) async {
await pumpBar(tester, affectedRows: 4);
expect(find.text('Rows affected: 4'), findsOneWidget);
});
});

testWidgets('shows status and affected empty modes', (tester) async {
await tester.pumpWidget(
resultsShell(
Expand Down
Loading
Loading