feat(settings): add quick messages management with drag-and-drop sorting
Adds a new "Quick Messages" settings page below Experts for managing reusable title/content snippets, backed by SQLite via SeaORM and exposed through both Tauri commands and the Axum web router. The list supports drag-to-reorder using the same motion/react Reorder pattern as the agent list, with translations provided across all 10 supported locales.
This commit is contained in:
@@ -9,3 +9,4 @@ pub mod folder_command;
|
||||
pub mod model_provider;
|
||||
pub mod opened_tab;
|
||||
pub mod prelude;
|
||||
pub mod quick_message;
|
||||
|
||||
@@ -10,3 +10,4 @@ pub use super::folder::Entity as Folder;
|
||||
pub use super::folder_command::Entity as FolderCommand;
|
||||
pub use super::model_provider::Entity as ModelProvider;
|
||||
pub use super::opened_tab::Entity as OpenedTab;
|
||||
pub use super::quick_message::Entity as QuickMessage;
|
||||
|
||||
19
src-tauri/src/db/entities/quick_message.rs
Normal file
19
src-tauri/src/db/entities/quick_message.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "quick_message")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub title: String,
|
||||
#[sea_orm(column_type = "Text")]
|
||||
pub content: String,
|
||||
pub sort_order: i32,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
70
src-tauri/src/db/migration/m20260424_000002_quick_message.rs
Normal file
70
src-tauri/src/db/migration/m20260424_000002_quick_message.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(QuickMessage::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(QuickMessage::Id)
|
||||
.integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(ColumnDef::new(QuickMessage::Title).string().not_null())
|
||||
.col(ColumnDef::new(QuickMessage::Content).text().not_null())
|
||||
.col(
|
||||
ColumnDef::new(QuickMessage::SortOrder)
|
||||
.integer()
|
||||
.not_null()
|
||||
.default(0),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(QuickMessage::CreatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(QuickMessage::UpdatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("idx_quick_message_sort_order")
|
||||
.table(QuickMessage::Table)
|
||||
.col(QuickMessage::SortOrder)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(QuickMessage::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum QuickMessage {
|
||||
Table,
|
||||
Id,
|
||||
Title,
|
||||
Content,
|
||||
SortOrder,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
}
|
||||
@@ -13,6 +13,7 @@ mod m20260420_000001_opened_tabs;
|
||||
mod m20260422_000001_folder_sort_order;
|
||||
mod m20260423_000001_drop_folder_parent_branch;
|
||||
mod m20260424_000001_folder_color;
|
||||
mod m20260424_000002_quick_message;
|
||||
pub struct Migrator;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
@@ -32,6 +33,7 @@ impl MigratorTrait for Migrator {
|
||||
Box::new(m20260422_000001_folder_sort_order::Migration),
|
||||
Box::new(m20260423_000001_drop_folder_parent_branch::Migration),
|
||||
Box::new(m20260424_000001_folder_color::Migration),
|
||||
Box::new(m20260424_000002_quick_message::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,5 +7,6 @@ pub mod folder_command_service;
|
||||
pub mod folder_service;
|
||||
pub mod import_service;
|
||||
pub mod model_provider_service;
|
||||
pub mod quick_message_service;
|
||||
pub mod sender_context_service;
|
||||
pub mod tab_service;
|
||||
|
||||
114
src-tauri/src/db/service/quick_message_service.rs
Normal file
114
src-tauri/src/db/service/quick_message_service.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
use chrono::Utc;
|
||||
use sea_orm::DatabaseConnection;
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ActiveValue::NotSet, ConnectionTrait, DbBackend, EntityTrait,
|
||||
IntoActiveModel, QueryOrder, Set, Statement,
|
||||
};
|
||||
|
||||
use crate::db::entities::quick_message;
|
||||
use crate::db::error::DbError;
|
||||
use crate::models::QuickMessageInfo;
|
||||
|
||||
fn to_info(m: quick_message::Model) -> QuickMessageInfo {
|
||||
QuickMessageInfo {
|
||||
id: m.id,
|
||||
title: m.title,
|
||||
content: m.content,
|
||||
sort_order: m.sort_order,
|
||||
created_at: m.created_at,
|
||||
updated_at: m.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list(conn: &DatabaseConnection) -> Result<Vec<QuickMessageInfo>, DbError> {
|
||||
let rows = quick_message::Entity::find()
|
||||
.order_by_asc(quick_message::Column::SortOrder)
|
||||
.all(conn)
|
||||
.await?;
|
||||
|
||||
Ok(rows.into_iter().map(to_info).collect())
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
conn: &DatabaseConnection,
|
||||
title: &str,
|
||||
content: &str,
|
||||
) -> Result<QuickMessageInfo, DbError> {
|
||||
let now = Utc::now();
|
||||
|
||||
let max_order = quick_message::Entity::find()
|
||||
.order_by_desc(quick_message::Column::SortOrder)
|
||||
.one(conn)
|
||||
.await?
|
||||
.map(|m| m.sort_order)
|
||||
.unwrap_or(-1);
|
||||
|
||||
let active = quick_message::ActiveModel {
|
||||
id: NotSet,
|
||||
title: Set(title.to_string()),
|
||||
content: Set(content.to_string()),
|
||||
sort_order: Set(max_order + 1),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
};
|
||||
|
||||
let model = active.insert(conn).await?;
|
||||
Ok(to_info(model))
|
||||
}
|
||||
|
||||
pub async fn update(
|
||||
conn: &DatabaseConnection,
|
||||
id: i32,
|
||||
title: Option<String>,
|
||||
content: Option<String>,
|
||||
) -> Result<QuickMessageInfo, DbError> {
|
||||
let row = quick_message::Entity::find_by_id(id)
|
||||
.one(conn)
|
||||
.await?
|
||||
.ok_or_else(|| DbError::Migration(format!("QuickMessage {} not found", id)))?;
|
||||
|
||||
let mut active = row.into_active_model();
|
||||
if let Some(t) = title {
|
||||
active.title = Set(t);
|
||||
}
|
||||
if let Some(c) = content {
|
||||
active.content = Set(c);
|
||||
}
|
||||
active.updated_at = Set(Utc::now());
|
||||
|
||||
let model = active.update(conn).await?;
|
||||
Ok(to_info(model))
|
||||
}
|
||||
|
||||
pub async fn delete(conn: &DatabaseConnection, id: i32) -> Result<(), DbError> {
|
||||
quick_message::Entity::delete_by_id(id).exec(conn).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reorder(conn: &DatabaseConnection, ids: Vec<i32>) -> Result<(), DbError> {
|
||||
if ids.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let now = Utc::now();
|
||||
let now_str = now.format("%Y-%m-%d %H:%M:%S %:z").to_string();
|
||||
let case_expr = ids
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, id)| format!("WHEN {} THEN {}", id, idx))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let id_list = ids
|
||||
.iter()
|
||||
.map(|id| id.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
let sql = format!(
|
||||
"UPDATE quick_message SET sort_order = CASE id {case_expr} END, updated_at = '{now_str}' WHERE id IN ({id_list})"
|
||||
);
|
||||
conn.execute(Statement::from_string(DbBackend::Sqlite, sql))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user