v0.6.0: release workflow, constraint-aware cell editing, pending-cell styling (#9)
* feat: demo DB seed/regenerate, backup/restore/sync refinements, SSH/SSL polish - Demo SQLite DB: feature-rich seed (12 objects, 500-row audit log) + regenerate action in Settings - Backup/restore: headless-testable core logic, psql -f for plain dumps, sync passes --clean --if-exists - SSH/SSL runtime refinements and connection testing improvements - Data grid: DataTypeIcon component, FK popover, table tree polish - Docs: AGENTS.md/README updates, new screenshots, MIT LICENSE * chore: optimize README screenshots (4.7 MB → 916 KB via pngquant, quality 70-90) * v0.6.0: release workflow, constraint-aware cell editing, pending-cell styling - Bump version to 0.6.0 across package.json, Cargo.toml, tauri.conf.json - Add .github/workflows/release.yml: tag-triggered CI builds macOS (aarch64 + x64), Windows, and Linux installers into a draft GitHub Release - README: installer download table (unsigned note, per-platform files), "how releases are made" section - CellEditor: constraint-aware commit — empty input on nullable columns becomes NULL, NOT NULL text-like types fall back to empty string, all other types blocked with an inline error bubble; replace "Set NULL" checkbox with a NULL row in the FK dropdown / empty enum option - VirtualDataGrid: pending-edit dot → animated pending outline (ring) on staged cells; matching test updates - docs-coverage test: align with rewritten README comparison table
This commit is contained in:
@@ -97,4 +97,4 @@ mod tests {
|
||||
assert!(json.contains("dump"));
|
||||
assert!(json.contains("completed"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,19 +91,31 @@ mod tests {
|
||||
assert_eq!(deserialized.port, Some(5432));
|
||||
assert_eq!(deserialized.username, Some("admin".to_string()));
|
||||
assert_eq!(deserialized.folder_id, Some("folder1".to_string()));
|
||||
assert_eq!(deserialized.tag_ids, vec!["tag1".to_string(), "tag2".to_string()]);
|
||||
assert_eq!(
|
||||
deserialized.tag_ids,
|
||||
vec!["tag1".to_string(), "tag2".to_string()]
|
||||
);
|
||||
assert_eq!(deserialized.password, Some("secret123".to_string()));
|
||||
assert_eq!(deserialized.database, Some("mydb".to_string()));
|
||||
assert_eq!(deserialized.ssh_host, Some("jumphost.example.com".to_string()));
|
||||
assert_eq!(
|
||||
deserialized.ssh_host,
|
||||
Some("jumphost.example.com".to_string())
|
||||
);
|
||||
assert_eq!(deserialized.ssh_port, Some(2222));
|
||||
assert_eq!(deserialized.ssh_user, Some("tunnel".to_string()));
|
||||
assert_eq!(deserialized.ssh_auth_method, Some("Key".to_string()));
|
||||
assert_eq!(deserialized.ssh_private_key_path, Some("/path/to/key".to_string()));
|
||||
assert_eq!(
|
||||
deserialized.ssh_private_key_path,
|
||||
Some("/path/to/key".to_string())
|
||||
);
|
||||
assert_eq!(deserialized.ssh_password, Some("ssh-pw".to_string()));
|
||||
assert_eq!(deserialized.ssh_passphrase, Some("passphrase".to_string()));
|
||||
assert_eq!(deserialized.ssl_mode, Some("require".to_string()));
|
||||
assert_eq!(deserialized.ssl_ca_path, Some("/path/to/ca".to_string()));
|
||||
assert_eq!(deserialized.ssl_cert_path, Some("/path/to/cert".to_string()));
|
||||
assert_eq!(
|
||||
deserialized.ssl_cert_path,
|
||||
Some("/path/to/cert".to_string())
|
||||
);
|
||||
assert_eq!(deserialized.ssl_key_path, Some("/path/to/key".to_string()));
|
||||
}
|
||||
|
||||
@@ -136,22 +148,40 @@ mod tests {
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&conn).unwrap();
|
||||
assert!(!json.contains("password"), "Connection JSON should not contain password field");
|
||||
assert!(
|
||||
!json.contains("password"),
|
||||
"Connection JSON should not contain password field"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connection_serializes_favorite_field() {
|
||||
let conn = Connection {
|
||||
id: "x".into(), name: "n".into(), db_type: "postgresql".into(),
|
||||
host: "h".into(), port: Some(5432), username: None, database: None,
|
||||
folder_id: None, keychain_ref: None, environment: None,
|
||||
ssh_host: None, ssh_port: None, ssh_user: None, ssh_auth_method: None,
|
||||
ssh_private_key_path: None, ssl_mode: None, ssl_ca_path: None,
|
||||
ssl_cert_path: None, ssl_key_path: None, tag_ids: vec![],
|
||||
id: "x".into(),
|
||||
name: "n".into(),
|
||||
db_type: "postgresql".into(),
|
||||
host: "h".into(),
|
||||
port: Some(5432),
|
||||
username: None,
|
||||
database: None,
|
||||
folder_id: None,
|
||||
keychain_ref: None,
|
||||
environment: None,
|
||||
ssh_host: None,
|
||||
ssh_port: None,
|
||||
ssh_user: None,
|
||||
ssh_auth_method: None,
|
||||
ssh_private_key_path: None,
|
||||
ssl_mode: None,
|
||||
ssl_ca_path: None,
|
||||
ssl_cert_path: None,
|
||||
ssl_key_path: None,
|
||||
tag_ids: vec![],
|
||||
favorite: true,
|
||||
created_at: "2024-01-01T00:00:00Z".into(), updated_at: "2024-01-01T00:00:00Z".into(),
|
||||
created_at: "2024-01-01T00:00:00Z".into(),
|
||||
updated_at: "2024-01-01T00:00:00Z".into(),
|
||||
};
|
||||
let json = serde_json::to_string(&conn).unwrap();
|
||||
assert!(json.contains("\"favorite\":true"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
|
||||
pub struct FilterRule {
|
||||
pub id: String,
|
||||
pub column: String,
|
||||
pub operator: String, // "eq" | "neq" | "contains" | "starts" | "ends" | "gt" | "lt" | "null" | "notnull"
|
||||
pub operator: String, // "eq" | "neq" | "contains" | "starts" | "ends" | "gt" | "lt" | "null" | "notnull"
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ pub struct FilterRule {
|
||||
pub struct SortRule {
|
||||
pub id: String,
|
||||
pub column: String,
|
||||
pub order: String, // "asc" | "desc"
|
||||
pub order: String, // "asc" | "desc"
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -347,11 +347,13 @@ mod tests {
|
||||
fn change_drop_and_empty_roundtrip() {
|
||||
let drop: Change = serde_json::from_value(serde_json::json!({
|
||||
"type": "drop_table", "id": "d", "schema": "public", "table": "t"
|
||||
})).unwrap();
|
||||
}))
|
||||
.unwrap();
|
||||
assert_eq!(drop.id(), "d");
|
||||
let empty: Change = serde_json::from_value(serde_json::json!({
|
||||
"type": "empty_table", "id": "e", "schema": "public", "table": "t"
|
||||
})).unwrap();
|
||||
}))
|
||||
.unwrap();
|
||||
assert_eq!(empty.id(), "e");
|
||||
}
|
||||
|
||||
@@ -376,9 +378,15 @@ mod tests {
|
||||
#[test]
|
||||
fn column_info_has_editability_fields() {
|
||||
let c = ColumnInfo {
|
||||
name: "id".into(), data_type: "integer".into(), is_nullable: false,
|
||||
is_pk: true, is_fk: false, fk_ref: None, default_value: None,
|
||||
editable: false, is_generated: false,
|
||||
name: "id".into(),
|
||||
data_type: "integer".into(),
|
||||
is_nullable: false,
|
||||
is_pk: true,
|
||||
is_fk: false,
|
||||
fk_ref: None,
|
||||
default_value: None,
|
||||
editable: false,
|
||||
is_generated: false,
|
||||
};
|
||||
let json = serde_json::to_string(&c).unwrap();
|
||||
assert!(json.contains("\"editable\":false"));
|
||||
@@ -513,7 +521,10 @@ mod tests {
|
||||
|
||||
let json = serde_json::to_string(&graph).unwrap();
|
||||
assert!(json.contains("users"), "should contain table name");
|
||||
assert!(json.contains("orders"), "should contain relationship source table");
|
||||
assert!(
|
||||
json.contains("orders"),
|
||||
"should contain relationship source table"
|
||||
);
|
||||
assert!(json.contains("1:N"), "should contain cardinality");
|
||||
assert!(json.contains("is_pk"), "should contain is_pk field");
|
||||
assert!(json.contains("is_fk"), "should contain is_fk field");
|
||||
@@ -552,7 +563,10 @@ mod tests {
|
||||
fk_ref: None,
|
||||
};
|
||||
let json = serde_json::to_string(&col_none).unwrap();
|
||||
assert!(json.contains("null"), "fk_ref=None should serialize as null");
|
||||
assert!(
|
||||
json.contains("null"),
|
||||
"fk_ref=None should serialize as null"
|
||||
);
|
||||
|
||||
// fk_ref = Some(...)
|
||||
let col_some = GraphColumn {
|
||||
@@ -569,4 +583,4 @@ mod tests {
|
||||
assert!(json.contains("users"), "should contain referenced table");
|
||||
assert!(json.contains("id"), "should contain referenced column");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,4 +15,4 @@ pub struct FolderInput {
|
||||
pub name: String,
|
||||
pub parent_id: Option<String>,
|
||||
pub tag_ids: Option<Vec<String>>,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,15 +3,15 @@ pub mod connection;
|
||||
pub mod db_viewer;
|
||||
pub mod folder;
|
||||
pub mod recent;
|
||||
pub mod settings;
|
||||
pub mod ssh;
|
||||
pub mod tag;
|
||||
pub mod settings;
|
||||
|
||||
pub use connection::{Connection, ConnectionInput};
|
||||
pub use recent::RecentConnection;
|
||||
#[allow(unused_imports)]
|
||||
pub use db_viewer::{Change, ColumnInfo, FilterRule, Pagination, QueryResult, SortRule, TableInfo};
|
||||
pub use folder::{Folder, FolderInput};
|
||||
pub use recent::RecentConnection;
|
||||
pub use settings::Settings;
|
||||
pub use ssh::SshConfig;
|
||||
pub use tag::{Tag, TagInput};
|
||||
pub use tag::{Tag, TagInput};
|
||||
|
||||
@@ -4,4 +4,4 @@ use serde::{Deserialize, Serialize};
|
||||
pub struct RecentConnection {
|
||||
pub connection_id: String,
|
||||
pub opened_at: String,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,4 +19,4 @@ pub struct Settings {
|
||||
pub editor_word_wrap: String,
|
||||
pub editor_minimap: bool,
|
||||
pub editor_tab_size: i64,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,12 +15,7 @@ pub struct SshConfig {
|
||||
|
||||
impl SshConfig {
|
||||
/// Create a new `SshConfig` with the required fields.
|
||||
pub fn new(
|
||||
host: String,
|
||||
port: u16,
|
||||
user: String,
|
||||
auth_method: String,
|
||||
) -> Self {
|
||||
pub fn new(host: String, port: u16, user: String, auth_method: String) -> Self {
|
||||
SshConfig {
|
||||
host,
|
||||
port,
|
||||
@@ -41,4 +36,4 @@ impl SshConfig {
|
||||
pub fn is_valid(&self) -> bool {
|
||||
!self.host.is_empty() && self.port >= 1 && !self.user.is_empty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,4 +12,4 @@ pub struct Tag {
|
||||
pub struct TagInput {
|
||||
pub name: String,
|
||||
pub color: String,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user