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
5 changes: 2 additions & 3 deletions linux/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ The app runs on GNOME 50 ([ADR 0015](docs/decisions/0015-gnome-50-platform-floor
| Filters | Per-column operators, a raw WHERE fragment checked as one expression, match any or all; binary columns compare to `0x` hex. Kept per table, and per tab across a restart or a reopen |
| Sorting | A header click sorts tables on the server and query results in the grid, one column at a time |
| 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. Table tabs only, on tables with a primary key; key, generated and auto-increment columns, and binary, spatial, array and composite values, are read-only |
| 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. On ClickHouse a save is not atomic, and Stop takes effect between statements |
| Rows | Insert, duplicate and delete; Paste (Ctrl+V) adds tab-separated rows from the clipboard, filling the columns from the focused one or by a header's names, or edits the focused cell with one value, and asks first above 1,000 rows, up to 100,000; Set NULL and Empty, and DEFAULT on a new row; undo and redo; save or discard pending changes, and stop a save. On ClickHouse a save is not atomic, and Stop takes effect between statements |
| Bulk delete | Deleting 10 rows or more asks first, unless Confirm Before Deleting Rows is off in Preferences |
| Copy | Copy; Copy As rows, with headers, JSON, CSV with or without headers, Markdown or IN clause, and INSERT in an editable table tab; Copy Column Name; Show Row as JSON |
| Foreign keys | Open Referenced Row (Ctrl+J) on a key cell opens the table it points at in a new tab, filtered on every column of the key; Show Referencing Rows on a saved row opens a table whose key points at it. A tab already showing those rows comes to the front |
Expand Down Expand Up @@ -134,7 +134,7 @@ The app runs on GNOME 50 ([ADR 0015](docs/decisions/0015-gnome-50-platform-floor
| Desktop integration | Desktop file with Open With for SQLite files, D-Bus activation, a man page, metainfo with screenshots, and SVG icons |
| Translations | Vietnamese ships. The app's own strings go through gettext, and CI fails when `POTFILES.in` is stale or a translation loses a placeholder. CI builds the template with gettext 1.0 and publishes it from main and with each release; translators send pull requests against `po/*.po` ([po/README.md](po/README.md#where-translators-work)) |
| Logs | stderr, which the session journals, with JSON lines on request. A panic also writes a crash report under `$XDG_STATE_HOME`, offered with Show Report on the next launch |
| Tests | Unit, gtk (with an accessibility audit of the picker, the connect pages, the workspace, the connect dialog and every preferences page), data and packaging suites; connection, DDL and TLS conformance, and a CSV and SQL INSERT export round trip, on every SQL engine, and keyspace and TLS conformance on Redis; Docker jobs for PostgreSQL with PgBouncer, MySQL and MariaDB, SQL Server, ClickHouse, Redis, SSH, the session and the grid save ([testing.md](docs/testing.md)); every critical audit finding has named guards ([regression-tests.md](docs/regression-tests.md)) |
| Tests | Unit, gtk (with an accessibility audit of the picker, the connect pages, the workspace, the connect dialog and every preferences page), data and packaging suites; connection, DDL and TLS conformance, and a CSV and SQL INSERT export round trip and a copy and paste round trip, on every SQL engine, and keyspace and TLS conformance on Redis; Docker jobs for PostgreSQL with PgBouncer, MySQL and MariaDB, SQL Server, ClickHouse, Redis, SSH, the session and the grid save ([testing.md](docs/testing.md)); every critical audit finding has named guards ([regression-tests.md](docs/regression-tests.md)) |

## Backlog

Expand All @@ -147,7 +147,6 @@ Not built yet, in no order.

### Browse, edit and structure

- Paste rows from the clipboard. Ctrl+V in the grid says it is not supported yet.
- Set a cell to `NOW()` or `CURRENT_TIMESTAMP`, which needs SQL expressions as cell values.
- Edit binary columns from and to a file. They are read-only today, as are array, spatial, composite and variant columns.
- Time and timestamp pickers. Dates have a calendar; times and timestamps are typed.
Expand Down
1 change: 1 addition & 0 deletions linux/changelog.d/3815.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Paste rows into a table with Ctrl+V or Paste in the cell menu, copied from a TablePro grid or a spreadsheet.
1 change: 1 addition & 0 deletions linux/changelog.d/3815.fixed.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A CSV import error quoted the whole refused value, however long it was.
1 change: 1 addition & 0 deletions linux/changelog.d/3815.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Duplicating several rows showed the copies in reverse order.
8 changes: 7 additions & 1 deletion linux/crates/app/src/application/accelerators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,13 @@ pub const ACCELERATORS: &[Accelerator] = &[
&["<Primary>c"],
Listed(DataGrid),
),
local(BrowseGrid, BUBBLE, "browse.paste-rows", &["<Primary>v"], Unlisted),
local(
BrowseGrid,
BUBBLE,
"browse.paste-rows",
&["<Primary>v"],
Listed(DataGrid),
),
local(
BrowseGrid,
BUBBLE,
Expand Down
16 changes: 6 additions & 10 deletions linux/crates/app/src/services/change_tracker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ impl TabChangeTracker {
if rows.is_empty() {
return self.delta(Vec::new());
}
let drafts: Vec<DraftRow> = rows
let mut drafts: Vec<DraftRow> = rows
.into_iter()
.map(|cells| {
let draft_id = self.next_draft_id;
Expand All @@ -276,6 +276,10 @@ impl TabChangeTracker {
}
})
.collect();
// Drafts are kept bottom up, the last one shown on top, so the
// rows go in last first to read down in their own order. Their
// touches keep that order for the save.
drafts.reverse();
self.push_undo(UndoOp::Insert { drafts: drafts.clone() });
self.put_drafts(drafts)
}
Expand Down Expand Up @@ -1031,15 +1035,6 @@ fn insert_cells(columns: &[ColumnInfo], draft: &DraftRow) -> Vec<CellInput> {
.collect()
}

/// What a new row holds in `column` until the user types: the server's
/// value where it has one, else NULL.
pub fn new_draft_cell(column: &ColumnInfo) -> CellInput {
match column.is_auto_increment || column.is_generated || !column.default.is_none() {
true => CellInput::Default,
false => CellInput::Value(Value::Null),
}
}

#[cfg(test)]
mod tests {
/// A column carrying the server's spelling and the kind it means,
Expand Down Expand Up @@ -1073,6 +1068,7 @@ mod tests {

use super::*;
use tablepro_core::ColumnInfo;
use tablepro_core::edit::new_draft_cell;

fn pk_col(name: &str) -> ColumnInfo {
ColumnInfo {
Expand Down
4 changes: 2 additions & 2 deletions linux/crates/app/src/services/column_relayout.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use tablepro_core::column::ColumnType;
use tablepro_core::edit::{CellInput, parse_literal_text};
use tablepro_core::edit::{CellInput, new_draft_cell, parse_literal_text};
use tablepro_core::export::value_text;
use tablepro_core::value::same_value;
use tablepro_core::{ColumnInfo, Value};

use super::change_tracker::{RowKey, new_draft_cell};
use super::change_tracker::RowKey;

/// How the columns a tab's pending changes were made against line up
/// with the columns it read since. Columns are matched by name: once a
Expand Down
11 changes: 10 additions & 1 deletion linux/crates/app/src/ui/browse_tab/browse_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,16 @@ pub(super) fn install_browse_actions(tab: &BrowseTab, page_size: u64) -> BrowseA
});
group.add_action(&page_size);
add("refresh", || PagingInput::RequestRefresh.into());
add("paste-rows", || ClipboardInput::PasteNotSupported.into());
let paste_rows = gio::SimpleAction::new("paste-rows", None);
let weak = tab.downgrade();
paste_rows.connect_activate(move |_, _| {
let Some(tab) = weak.upgrade() else {
return;
};
let at = tab.read(BrowseModel::focused_cell).flatten();
tab.send(ClipboardInput::Paste(at));
});
group.add_action(&paste_rows);
let previous = add("previous-page", || PagingInput::PrevPage.into());
let next = add("next-page", || PagingInput::NextPage.into());
tab.imp().pager_bar.follow_page_actions(ACTION_GROUP, &previous, &next);
Expand Down
2 changes: 2 additions & 0 deletions linux/crates/app/src/ui/browse_tab/browse_tab_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use super::clipboard_input::ClipboardInput;
use super::data_input::DataInput;
use super::edit_input::EditInput;
use super::paging_input::PagingInput;
use super::paste_at::PasteAt;
use super::save_input::SaveInput;
use super::selection_input::SelectionInput;
use crate::ui::grid::GridMsg;
Expand Down Expand Up @@ -70,6 +71,7 @@ impl From<GridMsg> for BrowseTabInput {
GridMsg::DeleteRows { rows } => EditInput::DeleteRows(rows).into(),
GridMsg::InsertRow => EditInput::InsertRow.into(),
GridMsg::DuplicateRows { rows } => EditInput::DuplicateRows(rows).into(),
GridMsg::Paste { at } => ClipboardInput::Paste(at.map(|(row, column)| PasteAt { row, column })).into(),
GridMsg::FollowReference(reference) => ClipboardInput::GridFollowReference(reference).into(),
}
}
Expand Down
10 changes: 4 additions & 6 deletions linux/crates/app/src/ui/browse_tab/clipboard_handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::ui::export::{ExportOffer, ExportOrigin, ExportQuery, ExportSource};
use crate::ui::grid::{GridRowId, RowObject, RowSnapshot, export_snapshot};

impl BrowseModel {
pub(super) fn on_clipboard(&self, input: ClipboardInput, fx: &mut Effects) {
pub(super) fn on_clipboard(&mut self, input: ClipboardInput, fx: &mut Effects) {
match input {
ClipboardInput::Export(page) => self.offer_export(page, fx),
ClipboardInput::GridCopy(request) => fx.output(BrowseTabOutput::Copy(request)),
Expand All @@ -22,11 +22,9 @@ impl BrowseModel {
))),
},
ClipboardInput::CopySelectedRowsAsTsv => self.copy_selected_rows(fx),
ClipboardInput::PasteNotSupported => {
fx.output(BrowseTabOutput::ShowToast(crate::i18n::gettext(
"Pasting rows isn't supported yet",
)));
}
ClipboardInput::Paste(at) => self.paste(at, fx),
ClipboardInput::PasteRead(read) => self.paste_read(*read, fx),
ClipboardInput::PasteAnswered { ticket, confirmed } => self.paste_answered(ticket, confirmed, fx),
ClipboardInput::GridShowRowJson(request) => fx.output(BrowseTabOutput::ShowRowJson(request)),
ClipboardInput::GridShowToast(message) => fx.output(BrowseTabOutput::ShowToast(message)),
ClipboardInput::GridFollowReference(reference) => fx.output(BrowseTabOutput::FollowReference(reference)),
Expand Down
14 changes: 11 additions & 3 deletions linux/crates/app/src/ui/browse_tab/clipboard_input.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use tablepro_core::browse::RowReference;

use super::paste_at::PasteAt;
use super::paste_read::PasteRead;
use crate::ui::clipboard::{CopyRequest, InClauseRows};
use crate::ui::export::ExportSource;
use crate::ui::grid::{RowJsonRequest, RowObject};

/// What the tab hands on and keeps no state for: copies in each format,
/// exports and the grid's notices.
/// Copies in each format, pastes, exports and the grid's notices.
#[derive(Debug)]
pub(crate) enum ClipboardInput {
/// The paginator's Export button, or the grid's Export with the page
Expand All @@ -15,7 +16,14 @@ pub(crate) enum ClipboardInput {
GridCopyRowsAsInsert(Vec<RowObject>),
GridCopyInClause(InClauseRows),
CopySelectedRowsAsTsv,
PasteNotSupported,
/// Ctrl+V or a menu's Paste, at the cell it names.
Paste(Option<PasteAt>),
PasteRead(Box<PasteRead>),
/// The user answered the question a large paste asked.
PasteAnswered {
ticket: u64,
confirmed: bool,
},
GridShowRowJson(RowJsonRequest),
GridShowToast(String),
GridFollowReference(RowReference),
Expand Down
12 changes: 12 additions & 0 deletions linux/crates/app/src/ui/browse_tab/confirming_paste.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use libadwaita as adw;
use tablepro_core::ColumnInfo;
use tablepro_core::import::PastedRows;

/// A paste large enough to ask about, waiting for the answer. Its cells
/// sit in the order of the columns they were read against.
pub(super) struct ConfirmingPaste {
pub(super) ticket: u64,
pub(super) rows: PastedRows,
pub(super) columns: Vec<ColumnInfo>,
pub(super) dialog: adw::AlertDialog,
}
7 changes: 4 additions & 3 deletions linux/crates/app/src/ui/browse_tab/edit_handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ use gtk4::prelude::*;
use libadwaita as adw;
use libadwaita::prelude::*;
use tablepro_core::Value;
use tablepro_core::edit::{CellInput, EditParseError, parse_edit_text};
use tablepro_core::edit::{CellInput, EditParseError, new_draft_cell, parse_edit_text};

use super::confirming_delete::ConfirmingDelete;
use super::row_deletion::{RowDeletion, plan_row_deletion};
use super::{BULK_DELETE_CONFIRM_THRESHOLD, BrowseModel, BrowseTabOutput, EditInput, Effects, apply_draft_changes};
use crate::services::change_tracker::{RowKey, new_draft_cell, pk_identity};
use crate::services::change_tracker::{RowKey, pk_identity};
use crate::services::column_relayout::ColumnRelayout;
use crate::services::tracker_delta::TrackerDelta;
use crate::services::tracker_edit::TrackerEdit;
Expand All @@ -28,6 +28,7 @@ impl BrowseModel {
fx.input(EditInput::FocusInsertedDraft);
}
EditInput::FocusInsertedDraft => self.focus_inserted_draft(),
EditInput::SelectPastedDrafts(count) => self.select_pasted_drafts(count),
EditInput::DeleteRows(rows) => self.delete_rows(&rows, fx),
EditInput::GridEdit { row, column, edit } => {
// The cell shows what was typed until its row is drawn again.
Expand Down Expand Up @@ -189,7 +190,7 @@ impl BrowseModel {
self.sync_delete_action();
}

fn apply_edit(&mut self, edit: TrackerEdit, fx: &mut Effects) {
pub(super) fn apply_edit(&mut self, edit: TrackerEdit, fx: &mut Effects) {
if self.tracker.refuses(&edit) && !std::mem::replace(&mut fx.told_edit_held, true) {
fx.output(BrowseTabOutput::ShowToast(crate::i18n::gettext(
"This row is being saved. Change it again once the save finishes.",
Expand Down
2 changes: 2 additions & 0 deletions linux/crates/app/src/ui/browse_tab/edit_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ pub(crate) enum EditInput {
/// Queued after an insert, so the focus lands on the column view
/// as it is once the insert is drawn.
FocusInsertedDraft,
/// Queued after a paste, as `FocusInsertedDraft` is after an insert.
SelectPastedDrafts(u32),
DeleteRows(Vec<RowObject>),
/// Checked against the cell's lock before the tracker takes it.
GridEdit {
Expand Down
Loading
Loading