Skip to content

Commit 1f49c4f

Browse files
authored
fix(linux): make the Confirm Before Deleting Rows switch take effect (#3448)
1 parent 1898e11 commit 1f49c4f

18 files changed

Lines changed: 222 additions & 69 deletions

File tree

‎linux/ROADMAP.md‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ The app runs on GNOME 50 ([ADR 0015](docs/decisions/0015-gnome-50-platform-floor
4747
| Sorting | A header click sorts tables on the server and query results in the grid |
4848
| Cell editing | Editors by column kind: inline text, a multi-line editor, a calendar for dates, a toggle for booleans, JSON checked before saving; values are edited from their exact text, never from what the cell displays |
4949
| Rows | Insert, duplicate and delete; Set NULL and Empty, and DEFAULT on a new row; undo and redo; save or discard pending changes, and stop a save |
50-
| Bulk delete | Deleting 10 rows or more asks first |
50+
| Bulk delete | Deleting 10 rows or more asks first, unless Confirm Before Deleting Rows is off in Preferences |
5151
| Copy | Copy, and Copy As rows, with headers, JSON, CSV, Markdown, IN clause or INSERT; Show Row as JSON |
5252
| Column widths | Kept per table |
5353
| CSV import | Into an existing table, with a column mapping, in one write batch |
@@ -102,7 +102,7 @@ The app runs on GNOME 50 ([ADR 0015](docs/decisions/0015-gnome-50-platform-floor
102102
| Session restore | The windows open at quit open again at launch, behind a preference |
103103
| Closing safely | Unsaved edits, a save in flight or a running export ask before a window closes or the app quits; logout waits while edits are unsaved, a save runs or an export writes |
104104
| Notifications | Work that finishes in a tab you are not looking at sends a notification |
105-
| Preferences | Restore session, page size, query history, editor font and scheme, query timeout. A Confirm Before Deleting Rows switch is shown but nothing reads it yet (see the backlog) |
105+
| Preferences | Restore session, page size, the bulk delete confirmation, query history, editor font and scheme, query timeout |
106106
| Shortcuts and About | A shortcuts dialog, and About from the metainfo with third-party licenses and debug info |
107107

108108
### Packaging and platform
@@ -132,7 +132,6 @@ Not built yet, in no order.
132132
- Edit binary columns from and to a file. They are read-only today.
133133
- Time and timestamp pickers. Dates have a calendar; times are typed.
134134
- Save the column order per table.
135-
- Make the Confirm Before Deleting Rows preference take effect. The switch is stored, but deleting rows does not read it.
136135
- Functions, procedures, sequences and triggers in the sidebar.
137136
- The CREATE INDEX statement for one index. Show CREATE TABLE lists them after the table on PostgreSQL, SQL Server and SQLite.
138137
- Jump from a foreign key to the table it references.

‎linux/crates/app/src/ui/browse_tab/edit_handlers.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ impl BrowseModel {
294294
if deletion.is_empty() {
295295
return;
296296
}
297-
if deletion.len() < BULK_DELETE_CONFIRM_THRESHOLD {
297+
if deletion.len() < BULK_DELETE_CONFIRM_THRESHOLD || !self.settings.confirm_bulk_delete() {
298298
fx.input(EditInput::MarkRowsDeleted(deletion));
299299
return;
300300
}

‎linux/crates/app/src/ui/browse_tab/load_tests.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -497,11 +497,11 @@ fn the_loading_page_names_a_table_with_an_ampersand() {
497497
outcome: Ok(rows(&["late"])),
498498
});
499499
let fake = crate::test_support::FakeSession::start(script, [], runtime.tasks());
500-
let root = tempfile::tempdir().expect("tempdir");
500+
let stores = super::tests::TabStores::new();
501501
let tab = super::tests::table_tab(
502502
&fake.session,
503503
&runtime,
504-
&root,
504+
&stores,
505505
"R&D",
506506
Default::default(),
507507
Default::default(),

‎linux/crates/app/src/ui/browse_tab/mod.rs‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ mod total_tests;
5454

5555
use std::cell::Cell;
5656
use std::collections::{BTreeSet, VecDeque};
57+
use std::rc::Rc;
5758
use std::sync::Arc;
5859

5960
use gtk4 as gtk;
@@ -67,6 +68,7 @@ use tablepro_core::meta::{RowIdentity, TableRef};
6768
use tablepro_session::operation::DeadlinePolicy;
6869
use tablepro_session::runtime::{Tasks, WatchForwarder};
6970
use tablepro_session::{LinkGeneration, Session};
71+
use tablepro_storage::AppSettings;
7072

7173
use self::browse_actions::BrowseActions;
7274
pub(crate) use self::browse_command::{BrowseCommand, ColumnsReply};
@@ -104,13 +106,8 @@ use crate::ui::export::ExportOffer;
104106

105107
const PAGE_SIZE_OPTIONS: &[u64] = &[100, 500, 1_000, 5_000, 10_000];
106108
const DEFAULT_PAGE_SIZE: u64 = 1_000;
107-
/// Bulk-delete safety net: when the user marks at least this many
108-
/// rows pending-delete in one shot, surface a confirmation dialog
109-
/// before tracking. The marker is reversible via Discard / Ctrl+Z,
110-
/// but a 200-row Ctrl+A → Delete sequence is destructive enough at
111-
/// a glance that an explicit confirmation matches GNOME Files'
112-
/// "Delete N items?" pattern.
113-
const BULK_DELETE_CONFIRM_THRESHOLD: usize = 10;
109+
/// A delete of fewer rows never asks, since Undo and Discard bring them back.
110+
pub(crate) const BULK_DELETE_CONFIRM_THRESHOLD: usize = 10;
114111

115112
glib::wrapper! {
116113
pub struct BrowseTab(ObjectSubclass<imp::BrowseTab>)
@@ -135,6 +132,7 @@ pub struct BrowseTabInit {
135132
pub tasks: Tasks,
136133
/// The session's deadlines, which a count that ran out names.
137134
pub deadlines: Arc<dyn DeadlinePolicy>,
135+
pub settings: Rc<AppSettings>,
138136
}
139137

140138
impl BrowseTab {
@@ -183,6 +181,7 @@ impl BrowseTab {
183181
filter_settings: init.filter_settings,
184182
pager: BrowsePager::new(init.page_size, init.initial_offset),
185183
deadlines: init.deadlines,
184+
settings: init.settings,
186185
current_sort: init.initial_sort,
187186
current_filter: initial_filter,
188187
current_columns: Vec::new(),
@@ -488,6 +487,8 @@ pub(crate) struct BrowseModel {
488487
/// Where the page the tab reads starts, and how many rows it holds.
489488
pager: BrowsePager,
490489
deadlines: Arc<dyn DeadlinePolicy>,
490+
/// Read at each delete, so a change in Preferences reaches open tabs.
491+
settings: Rc<AppSettings>,
491492
current_sort: Option<(usize, bool)>,
492493
/// The WHERE every page and count reads under, kept per connection,
493494
/// schema and table.

‎linux/crates/app/src/ui/browse_tab/tests.rs‎

Lines changed: 83 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use crate::services::change_tracker::{RowKey, StatementSource};
2121
use crate::services::tab_tracker_handle::TabTrackerHandle;
2222
use crate::services::tracker_delta::TrackerDelta;
2323
use crate::services::tracker_edit::TrackerEdit;
24-
use crate::test_support::{FakeSession, after_next_frame, drain_main_context, wait_until};
24+
use crate::test_support::{FakeSession, MemorySettings, after_next_frame, drain_main_context, wait_until};
2525
use crate::ui::clipboard::InsertRow;
2626
use crate::ui::grid::{GridCell, GridRowIndex, RowObject};
2727

@@ -152,7 +152,7 @@ pub(super) struct Harness {
152152
pub(super) session: Arc<Session>,
153153
pub(super) connection: Arc<FakeConnection>,
154154
pub(super) fake: FakeSession,
155-
root: tempfile::TempDir,
155+
pub(super) stores: TabStores,
156156
runtime: Option<crate::runtime::AppRuntime>,
157157
}
158158

@@ -184,17 +184,17 @@ impl Harness {
184184
crate::register_test_resources();
185185
let runtime = crate::test_support::test_runtime();
186186
let fake = FakeSession::start(script, reconnects, runtime.tasks());
187-
let root = tempfile::tempdir().expect("tempdir");
187+
let stores = TabStores::new();
188188
let tracker = TabTrackerHandle::default();
189-
let tab = filtered_users_tab(&fake.session, &runtime, &root, saved, tracker.clone(), read_only);
189+
let tab = filtered_users_tab(&fake.session, &runtime, &stores, saved, tracker.clone(), read_only);
190190
Self {
191191
tab: Some(tab),
192192
tracker,
193193
others: Vec::new(),
194194
session: fake.session.clone(),
195195
connection: fake.connection.clone(),
196196
fake,
197-
root,
197+
stores,
198198
runtime: Some(runtime),
199199
}
200200
}
@@ -206,7 +206,7 @@ impl Harness {
206206
/// Another tab on the same table and session.
207207
pub(super) fn open_another(&mut self) -> BrowseTab {
208208
let runtime = self.runtime.as_ref().expect("the runtime is running");
209-
let tab = users_tab(&self.session, runtime, &self.root);
209+
let tab = users_tab(&self.session, runtime, &self.stores);
210210
self.others.push(tab.clone());
211211
tab
212212
}
@@ -298,15 +298,26 @@ impl Drop for Shown {
298298
}
299299
}
300300

301-
pub(super) fn users_tab(
302-
session: &Arc<Session>,
303-
runtime: &crate::runtime::AppRuntime,
304-
root: &tempfile::TempDir,
305-
) -> BrowseTab {
301+
/// The files and preferences a test tab reads.
302+
pub(super) struct TabStores {
303+
root: tempfile::TempDir,
304+
pub(super) settings: MemorySettings,
305+
}
306+
307+
impl TabStores {
308+
pub(super) fn new() -> Self {
309+
Self {
310+
root: tempfile::tempdir().expect("tempdir"),
311+
settings: MemorySettings::new(),
312+
}
313+
}
314+
}
315+
316+
pub(super) fn users_tab(session: &Arc<Session>, runtime: &crate::runtime::AppRuntime, stores: &TabStores) -> BrowseTab {
306317
filtered_users_tab(
307318
session,
308319
runtime,
309-
root,
320+
stores,
310321
FilterSet::default(),
311322
TabTrackerHandle::default(),
312323
false,
@@ -316,26 +327,27 @@ pub(super) fn users_tab(
316327
pub(super) fn filtered_users_tab(
317328
session: &Arc<Session>,
318329
runtime: &crate::runtime::AppRuntime,
319-
root: &tempfile::TempDir,
330+
stores: &TabStores,
320331
saved: FilterSet,
321332
tracker: TabTrackerHandle,
322333
read_only: bool,
323334
) -> BrowseTab {
324-
table_tab(session, runtime, root, "users", saved, tracker, read_only)
335+
table_tab(session, runtime, stores, "users", saved, tracker, read_only)
325336
}
326337

327338
pub(super) fn table_tab(
328339
session: &Arc<Session>,
329340
runtime: &crate::runtime::AppRuntime,
330-
root: &tempfile::TempDir,
341+
stores: &TabStores,
331342
table: &str,
332343
saved: FilterSet,
333344
tracker: TabTrackerHandle,
334345
read_only: bool,
335346
) -> BrowseTab {
336347
let tasks = runtime.tasks();
337348
let connection_id = uuid::Uuid::new_v4();
338-
let filter_settings = crate::persistence::FilterSettingsStore::load(root.path().join("filters.json"), &tasks);
349+
let root = stores.root.path();
350+
let filter_settings = crate::persistence::FilterSettingsStore::load(root.join("filters.json"), &tasks);
339351
filter_settings.set_filters(connection_id, None, table, saved);
340352
BrowseTab::new(BrowseTabInit {
341353
tracker,
@@ -346,13 +358,14 @@ pub(super) fn table_tab(
346358
page_size: 100,
347359
initial_offset: 0,
348360
initial_sort: None,
349-
column_widths: crate::persistence::ColumnWidthStore::load(root.path().join("column-widths.json"), &tasks),
361+
column_widths: crate::persistence::ColumnWidthStore::load(root.join("column-widths.json"), &tasks),
350362
filter_settings,
351363
session: session.clone(),
352364
tasks,
353365
deadlines: Arc::new(tablepro_session::operation::DefaultDeadlines::new(
354366
tablepro_core::LivenessPolicy::DESKTOP,
355367
)),
368+
settings: stores.settings.get().clone(),
356369
})
357370
}
358371

@@ -2868,6 +2881,59 @@ fn a_second_delete_replaces_the_waiting_confirmation() {
28682881
assert_eq!(harness.tracker.pending_count(), DOZEN.len());
28692882
}
28702883

2884+
#[gtk4::test]
2885+
fn a_bulk_delete_with_the_confirmation_off_marks_its_rows_at_once() {
2886+
let mut script = serving_table();
2887+
script.query_bound.always(FakeReply::ok(rows(&DOZEN)));
2888+
let harness = Harness::open(script, []);
2889+
let _shown = Shown::new(harness.tab());
2890+
wait_until(WAIT, || harness.shown().len() == DOZEN.len()).expect("the first page");
2891+
2892+
harness
2893+
.stores
2894+
.settings
2895+
.get()
2896+
.set_confirm_bulk_delete(false)
2897+
.expect("turn the confirmation off");
2898+
harness.selection().select_all();
2899+
delete_rows(harness.tab());
2900+
drain_main_context();
2901+
2902+
assert!(
2903+
confirming(harness.tab()).is_none(),
2904+
"the delete asked with the switch off"
2905+
);
2906+
assert_eq!(harness.tracker.pending_count(), DOZEN.len());
2907+
}
2908+
2909+
#[gtk4::test]
2910+
fn turning_the_confirmation_back_on_asks_again_in_an_open_tab() {
2911+
let mut script = serving_table();
2912+
script.query_bound.always(FakeReply::ok(rows(&DOZEN)));
2913+
let harness = Harness::open(script, []);
2914+
let _shown = Shown::new(harness.tab());
2915+
let settings = harness.stores.settings.get();
2916+
settings
2917+
.set_confirm_bulk_delete(false)
2918+
.expect("turn the confirmation off");
2919+
wait_until(WAIT, || harness.shown().len() == DOZEN.len()).expect("the first page");
2920+
harness.selection().select_all();
2921+
delete_rows(harness.tab());
2922+
drain_main_context();
2923+
assert_eq!(harness.tracker.pending_count(), DOZEN.len());
2924+
harness.tab().send(EditInput::Undo);
2925+
assert_eq!(harness.tracker.pending_count(), 0);
2926+
2927+
settings
2928+
.set_confirm_bulk_delete(true)
2929+
.expect("turn the confirmation on");
2930+
let dialog = ask_to_delete_every_row(&harness);
2931+
crate::test_support::respond(&dialog, "delete");
2932+
drain_main_context();
2933+
2934+
assert_eq!(harness.tracker.pending_count(), DOZEN.len());
2935+
}
2936+
28712937
#[gtk4::test]
28722938
fn insert_row_opens_the_new_rows_first_editable_cell() {
28732939
let (harness, shown) = one_cell(named_col("name", "text", true), Value::Text("Ada".into()));

‎linux/crates/app/src/ui/connect_dialog/imp.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ pub struct ConnectDialog {
7575
pub mode: RefCell<ConnectDialogMode>,
7676
/// An Edit's save in flight, which keeps Save from running twice.
7777
pub saving: Cell<bool>,
78+
/// The user can close the dialog while a save runs, and the save
79+
/// must not close it again when it lands.
80+
pub has_closed: Cell<bool>,
7881
pub pem: PemValidation,
7982
/// The TLS settings the last file check read, so a keystroke in an
8083
/// unrelated row does not read the files again.
@@ -124,6 +127,7 @@ impl WidgetImpl for ConnectDialog {}
124127

125128
impl AdwDialogImpl for ConnectDialog {
126129
fn closed(&self) {
130+
self.has_closed.set(true);
127131
self.tests.cancel();
128132
self.testing.set(false);
129133
self.parent_closed();

‎linux/crates/app/src/ui/connect_dialog/mod.rs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -673,7 +673,9 @@ impl ConnectDialog {
673673
match outcome {
674674
Ok(Ok(saved)) => {
675675
dialog.emit_by_name::<()>("saved", &[&saved.id.to_string()]);
676-
dialog.close();
676+
if !dialog.imp().has_closed.get() {
677+
dialog.close();
678+
}
677679
}
678680
Ok(Err(error)) => {
679681
tracing::warn!(%error, "could not save the edited connection");

0 commit comments

Comments
 (0)