Files
胡飞 fd43db5c94 feat(home): 统一首页网格布局、树形视图与克制视觉层次
- 统一网格:card_grid_metrics 由内容区宽度计算列数/列宽,单项组与
  末行不拉宽;最近区容量跟随网格列数
- 主页卡片/列表/树形三视图,视图切换改为带勾选态的下拉菜单
- 树形视图嵌入主页:侧栏实现 Render 并按 home_embedded 输出,修复
  HomePage render 租约内 read(HomePage) 重入 panic;嵌入时隐藏树头部
- 团队标签中性化(muted 底),移入名称行与名称对齐;卡片/列表选中态
  统一为 list_active 语义色,去掉常驻阴影与逐项「最近」角标
- 最近区改用历史语义图标;提示仅在分组筛选生效时显示;计数精简为数字
- 展开/折叠全部归入「分组」菜单;新建连接降为 outline
- 侧栏收窄至 184px,折叠控制移入底部与设置同行;账户行去常驻邮箱
- 常驻连接侧栏仅非主页显示;合并 dev 并完成 universal-plugins 迁移
- 清理死代码:user_avatar 模块、Inline 变体、旧同步按钮状态机、
  冲突对话框等;full test 539 通过、clippy 无告警
2026-09-06 18:19:35 +08:00

209 lines
7.6 KiB
Rust

use db::connection::DbConnection;
use db::executor::{ExecOptions, SqlResult};
use db::plugin::DatabasePlugin;
use db::postgresql::PostgresPlugin;
use db::types::{TableCellChange, TableCellValue, TableRowChange, TableSaveRequest};
use crate::real_databases::common::env::{optional_database, postgres_config, skip_database};
use crate::real_databases::postgres::core_flow::{
drop_schema, execute, reset_schema, unique_schema,
};
#[tokio::test]
async fn postgres_real_table_data_pagination_filter_sort_and_crud() {
let Some(config) = postgres_config() else {
skip_database(
"PostgreSQL",
"ONETCLI_TEST_POSTGRES_PASSWORD (empty string is valid)",
);
return;
};
let config = optional_database(
&config,
&std::env::var("ONETCLI_TEST_POSTGRES_DATABASE").unwrap_or_else(|_| "postgres".to_string()),
);
let schema = unique_schema("data");
let plugin = PostgresPlugin::new();
let mut connection = plugin
.create_connection(config)
.await
.expect("PostgreSQL should connect");
reset_schema(&plugin, connection.as_ref(), &schema).await;
setup(&plugin, connection.as_ref(), &schema).await;
connection
.switch_schema(&schema)
.await
.expect("switch to data schema");
assert_table_data(&plugin, connection.as_ref(), &schema).await;
execute_generated_crud(&plugin, connection.as_ref(), &schema).await;
drop_schema(&plugin, connection.as_ref(), &schema).await;
connection.disconnect().await.expect("disconnect");
}
async fn setup(
plugin: &PostgresPlugin,
connection: &(dyn DbConnection + Send + Sync),
schema: &str,
) {
execute(
plugin,
connection,
&format!(
"CREATE SCHEMA IF NOT EXISTS \"{schema}\";\nCREATE TABLE \"{schema}\".people (\
id INT PRIMARY KEY, name TEXT NOT NULL, age INT, payload BYTEA, score NUMERIC(8,2), \
active BOOLEAN);\nINSERT INTO \"{schema}\".people VALUES \
(1, 'Alice', 30, decode('0102', 'hex'), 91.50, true), \
(2, 'Bob', 25, ''::bytea, NULL, false), \
(3, '中文', NULL, NULL, NULL, NULL);"
),
)
.await;
}
async fn assert_table_data(
plugin: &PostgresPlugin,
connection: &(dyn DbConnection + Send + Sync),
schema: &str,
) {
let first = db::types::TableDataRequest::new("current_database", "people")
.with_schema(schema)
.with_page(1, 2)
.with_where_clause("id >= 1")
.with_order_by_clause("id DESC");
let response = plugin
.query_table_data(connection, first)
.await
.expect("PostgreSQL table data");
assert_eq!(response.total_count, 3);
assert_eq!(
response.query_result.columns,
vec!["id", "name", "age", "payload", "score", "active"]
);
assert_eq!(response.query_result.rows.len(), 2);
assert_eq!(response.query_result.rows[0][0].as_deref(), Some("3"));
let second = db::types::TableDataRequest::new("current_database", "people")
.with_schema(schema)
.with_page(2, 1)
.with_offset(2)
.with_known_total_count(3)
.with_order_by_clause("id");
let response = plugin
.query_table_data(connection, second)
.await
.expect("PostgreSQL second page");
assert_eq!(response.total_count, 3);
assert_eq!(response.query_result.rows.len(), 1);
assert_eq!(response.query_result.rows[0][1].as_deref(), Some("中文"));
let filtered = db::types::TableDataRequest::new("current_database", "people")
.with_schema(schema)
.with_page(1, 100)
.with_where_clause("name LIKE 'A%'")
.with_order_by_clause("id");
let response = plugin
.query_table_data(connection, filtered)
.await
.expect("PostgreSQL filtered table data");
assert_eq!(response.total_count, 1);
assert_eq!(response.query_result.rows[0][1].as_deref(), Some("Alice"));
}
async fn execute_generated_crud(
plugin: &PostgresPlugin,
connection: &(dyn DbConnection + Send + Sync),
schema: &str,
) {
let columns = plugin
.list_columns(connection, "postgres", Some(schema.to_string()), "people")
.await
.expect("PostgreSQL people columns");
let indexes = plugin
.list_indexes(connection, "postgres", Some(schema.to_string()), "people")
.await
.expect("PostgreSQL people indexes");
let request = TableSaveRequest {
database: std::env::var("ONETCLI_TEST_POSTGRES_DATABASE")
.unwrap_or_else(|_| "postgres".to_string()),
schema: Some(schema.to_string()),
table: "people".to_string(),
columns: columns.clone(),
index_infos: indexes,
changes: vec![
TableRowChange::Added {
data: vec![
TableCellValue::Text("4".into()),
TableCellValue::Text("O'Reilly 🚀".into()),
TableCellValue::Text("41".into()),
TableCellValue::Binary(vec![0, 255]),
TableCellValue::Text("77.25".into()),
TableCellValue::Text("true".into()),
],
},
TableRowChange::Updated {
original_data: row(&columns, vec!["1", "Alice", "30", "0102", "91.50", "true"]),
changes: vec![TableCellChange {
column_index: 1,
column_name: "name".into(),
old_value: TableCellValue::Text("Alice".into()),
new_value: TableCellValue::Text("Alice Renamed 🚀".into()),
}],
rowid: None,
},
TableRowChange::Deleted {
original_data: row(&columns, vec!["2", "Bob", "25", "", "NULL", "false"]),
rowid: None,
},
],
};
let sql = plugin.generate_table_changes_sql(&request);
assert!(sql.contains("INSERT INTO"));
assert!(sql.contains("UPDATE"));
assert!(sql.contains("DELETE FROM"));
let results = connection
.execute(plugin, &sql, ExecOptions::default())
.await
.expect("PostgreSQL generated CRUD should execute");
crate::real_databases::common::assertions::assert_no_sql_errors(&results, &sql);
let result = connection
.query(&format!(
"SELECT name, age, encode(payload, 'hex'), score FROM \"{schema}\".people \
WHERE id IN (1,4) ORDER BY id"
))
.await
.expect("verify PostgreSQL CRUD");
let SqlResult::Query(result) = result else {
panic!("PostgreSQL CRUD verification should be a query");
};
assert_eq!(result.rows[0][0].as_deref(), Some("Alice Renamed 🚀"));
assert_eq!(result.rows[0][1].as_deref(), Some("30"));
assert_eq!(result.rows[1][0].as_deref(), Some("O'Reilly 🚀"));
assert_eq!(result.rows[1][2].as_deref(), Some("00ff"));
}
fn row(columns: &[db::types::ColumnInfo], values: Vec<&str>) -> Vec<TableCellValue> {
columns
.iter()
.zip(values)
.map(|(column, value)| {
if value == "NULL" {
TableCellValue::Null
} else if column.name == "payload" {
TableCellValue::Binary(
(0..value.len())
.step_by(2)
.map(|index| {
u8::from_str_radix(&value[index..index + 2], 16)
.expect("valid binary hex")
})
.collect(),
)
} else {
TableCellValue::Text(value.to_string())
}
})
.collect()
}