beads-kanban-ui 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -222
- package/package.json +18 -55
- package/.designs/beads-kanban-ui-bj0.md +0 -73
- package/.designs/beads-kanban-ui-qxq.md +0 -144
- package/.designs/epic-support.md +0 -282
- package/.env.local.example +0 -2
- package/.eslintrc.json +0 -3
- package/.gitattributes +0 -3
- package/.github/workflows/release.yml +0 -123
- package/.history/README_20260121193710.md +0 -227
- package/.history/README_20260121193918.md +0 -227
- package/.history/README_20260121193921.md +0 -227
- package/.history/README_20260121193933.md +0 -227
- package/.history/README_20260121193934.md +0 -227
- package/.history/README_20260121193944.md +0 -227
- package/.history/README_20260121193953.md +0 -227
- package/.history/src/app/page_20260121133429.tsx +0 -134
- package/.history/src/app/page_20260121133928.tsx +0 -134
- package/.history/src/app/page_20260121144850.tsx +0 -138
- package/.history/src/app/page_20260121144854.tsx +0 -138
- package/.history/src/app/page_20260121144858.tsx +0 -138
- package/.history/src/app/page_20260121144902.tsx +0 -138
- package/.history/src/app/page_20260121144906.tsx +0 -138
- package/.history/src/app/page_20260121144911.tsx +0 -138
- package/.history/src/app/page_20260121144928.tsx +0 -138
- package/.playwright-mcp/.playwright-mcp/morphing-dialog-wheel-scroll-fix.png +0 -0
- package/.playwright-mcp/beams-test.png +0 -0
- package/.playwright-mcp/card-verification.png +0 -0
- package/.playwright-mcp/design-doc-dialog-fix-verification.png +0 -0
- package/.playwright-mcp/dialog-width-test.png +0 -0
- package/.playwright-mcp/homepage.png +0 -0
- package/.playwright-mcp/morphing-dialog-expanded.png +0 -0
- package/.playwright-mcp/morphing-dialog-fixes-final.png +0 -0
- package/.playwright-mcp/morphing-dialog-open.png +0 -0
- package/.playwright-mcp/page-2026-01-21T14-08-31-529Z.png +0 -0
- package/.playwright-mcp/page-2026-01-21T14-09-23-431Z.png +0 -0
- package/.playwright-mcp/page-2026-01-21T14-10-28-773Z.png +0 -0
- package/.playwright-mcp/page-2026-01-21T14-10-47-432Z.png +0 -0
- package/.playwright-mcp/page-2026-01-21T14-11-12-350Z.png +0 -0
- package/.playwright-mcp/screenshot-after-click.png +0 -0
- package/.playwright-mcp/screenshot-after-dialog-click.png +0 -0
- package/.playwright-mcp/sheet-restored-after-dialog-close.png +0 -0
- package/.playwright-mcp/test-1-sheet-open-with-overlay.png +0 -0
- package/.playwright-mcp/test-2-morphing-dialog-with-overlay.png +0 -0
- package/.playwright-mcp/test-3-sheet-open-dark-overlay.png +0 -0
- package/.playwright-mcp/test-4-morphing-dialog-with-dark-overlay.png +0 -0
- package/.playwright-mcp/test-5-morphing-dialog-scrolled.png +0 -0
- package/.playwright-mcp/test-6-sheet-restored-after-dialog-close.png +0 -0
- package/.playwright-mcp/wheel-scroll-fixed.png +0 -0
- package/Screenshots/bead-detail.png +0 -0
- package/Screenshots/dashboard.png +0 -0
- package/Screenshots/kanban-board.png +0 -0
- package/components.json +0 -27
- package/logo/logo.svg +0 -1
- package/next.config.js +0 -9
- package/npm/README.md +0 -37
- package/npm/package.json +0 -20
- package/postcss.config.js +0 -6
- package/public/logo.svg +0 -1
- package/restart.sh +0 -5
- package/server/Cargo.lock +0 -1685
- package/server/Cargo.toml +0 -24
- package/server/src/db.rs +0 -570
- package/server/src/main.rs +0 -141
- package/server/src/routes/beads.rs +0 -413
- package/server/src/routes/cli.rs +0 -150
- package/server/src/routes/fs.rs +0 -360
- package/server/src/routes/git.rs +0 -169
- package/server/src/routes/mod.rs +0 -107
- package/server/src/routes/projects.rs +0 -177
- package/server/src/routes/watch.rs +0 -211
- package/src/app/globals.css +0 -101
- package/src/app/layout.tsx +0 -36
- package/src/app/page.tsx +0 -348
- package/src/app/project/kanban-board.tsx +0 -356
- package/src/app/project/page.tsx +0 -18
- package/src/app/settings/page.tsx +0 -224
- package/src/components/Beams.css +0 -5
- package/src/components/Beams.jsx +0 -307
- package/src/components/Galaxy.css +0 -5
- package/src/components/Galaxy.jsx +0 -333
- package/src/components/activity-timeline.tsx +0 -172
- package/src/components/add-project-dialog.tsx +0 -219
- package/src/components/bead-card.tsx +0 -196
- package/src/components/bead-detail.tsx +0 -306
- package/src/components/color-picker.tsx +0 -101
- package/src/components/comment-input.tsx +0 -155
- package/src/components/comment-list.tsx +0 -147
- package/src/components/dependency-badge.tsx +0 -106
- package/src/components/design-doc-dialog.tsx +0 -58
- package/src/components/design-doc-preview.tsx +0 -97
- package/src/components/design-doc-viewer.tsx +0 -199
- package/src/components/editable-project-name.tsx +0 -178
- package/src/components/epic-card.tsx +0 -263
- package/src/components/folder-browser.tsx +0 -273
- package/src/components/footer.tsx +0 -27
- package/src/components/kanban/default.tsx +0 -184
- package/src/components/kanban-column.tsx +0 -167
- package/src/components/project-card.tsx +0 -191
- package/src/components/quick-filter-bar.tsx +0 -279
- package/src/components/scan-directory-dialog.tsx +0 -368
- package/src/components/status-donut.tsx +0 -197
- package/src/components/subtask-list.tsx +0 -128
- package/src/components/tag-picker.tsx +0 -252
- package/src/components/ui/.gitkeep +0 -0
- package/src/components/ui/alert-dialog.tsx +0 -141
- package/src/components/ui/avatar.tsx +0 -67
- package/src/components/ui/badge.tsx +0 -230
- package/src/components/ui/button.tsx +0 -433
- package/src/components/ui/card/index.tsx +0 -24
- package/src/components/ui/card/roiui-card.module.css +0 -197
- package/src/components/ui/card/roiui-card.tsx +0 -154
- package/src/components/ui/card/shadcn-card.tsx +0 -76
- package/src/components/ui/chart.tsx +0 -369
- package/src/components/ui/dialog.tsx +0 -122
- package/src/components/ui/dropdown-menu.tsx +0 -201
- package/src/components/ui/input.tsx +0 -22
- package/src/components/ui/kanban.tsx +0 -522
- package/src/components/ui/morphing-dialog.tsx +0 -457
- package/src/components/ui/popover.tsx +0 -33
- package/src/components/ui/progress.tsx +0 -28
- package/src/components/ui/scroll-area.tsx +0 -48
- package/src/components/ui/select.tsx +0 -159
- package/src/components/ui/separator.tsx +0 -31
- package/src/components/ui/sheet.tsx +0 -142
- package/src/components/ui/skeleton.tsx +0 -15
- package/src/components/ui/toast.tsx +0 -129
- package/src/components/ui/toaster.tsx +0 -35
- package/src/components/ui/tooltip.tsx +0 -30
- package/src/hooks/.gitkeep +0 -0
- package/src/hooks/use-bead-filters.ts +0 -261
- package/src/hooks/use-beads.ts +0 -162
- package/src/hooks/use-branch-statuses.ts +0 -161
- package/src/hooks/use-epics.ts +0 -173
- package/src/hooks/use-file-watcher.ts +0 -111
- package/src/hooks/use-keyboard-navigation.ts +0 -282
- package/src/hooks/use-project.ts +0 -61
- package/src/hooks/use-projects.ts +0 -93
- package/src/hooks/use-toast.ts +0 -194
- package/src/hooks/useClickOutside.tsx +0 -26
- package/src/lib/.gitkeep +0 -0
- package/src/lib/api.ts +0 -186
- package/src/lib/beads-parser.ts +0 -252
- package/src/lib/cli.ts +0 -193
- package/src/lib/db.ts +0 -145
- package/src/lib/design-doc.ts +0 -74
- package/src/lib/epic-parser.ts +0 -242
- package/src/lib/git.ts +0 -102
- package/src/lib/utils.ts +0 -12
- package/src/types/index.ts +0 -107
- package/tailwind.config.ts +0 -85
- package/tsconfig.json +0 -26
- /package/{npm/bin → bin}/cli.js +0 -0
- /package/{npm/scripts → scripts}/postinstall.js +0 -0
package/server/Cargo.toml
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
[package]
|
|
2
|
-
name = "beads-server"
|
|
3
|
-
version = "0.1.0"
|
|
4
|
-
edition = "2021"
|
|
5
|
-
|
|
6
|
-
[dependencies]
|
|
7
|
-
axum = "0.7"
|
|
8
|
-
tokio = { version = "1", features = ["full"] }
|
|
9
|
-
tower-http = { version = "0.5", features = ["cors", "fs"] }
|
|
10
|
-
serde = { version = "1", features = ["derive"] }
|
|
11
|
-
serde_json = "1"
|
|
12
|
-
tracing = "0.1"
|
|
13
|
-
tracing-subscriber = "0.3"
|
|
14
|
-
rust-embed = "8"
|
|
15
|
-
open = "5"
|
|
16
|
-
mime_guess = "2"
|
|
17
|
-
notify = "6"
|
|
18
|
-
tokio-stream = "0.1"
|
|
19
|
-
futures = "0.3"
|
|
20
|
-
rusqlite = { version = "0.31", features = ["bundled"] }
|
|
21
|
-
uuid = { version = "1", features = ["v4"] }
|
|
22
|
-
chrono = { version = "0.4", features = ["serde"] }
|
|
23
|
-
thiserror = "1"
|
|
24
|
-
directories = "5"
|
package/server/src/db.rs
DELETED
|
@@ -1,570 +0,0 @@
|
|
|
1
|
-
//! Database module for beads-server
|
|
2
|
-
//!
|
|
3
|
-
//! Provides SQLite storage for projects, tags, and their relationships.
|
|
4
|
-
//! Uses rusqlite with Arc<Mutex<>> for thread-safe access from Axum handlers.
|
|
5
|
-
|
|
6
|
-
use chrono::Utc;
|
|
7
|
-
use rusqlite::{params, Connection, Result as SqliteResult};
|
|
8
|
-
use serde::{Deserialize, Serialize};
|
|
9
|
-
use std::path::PathBuf;
|
|
10
|
-
use std::sync::Mutex;
|
|
11
|
-
use thiserror::Error;
|
|
12
|
-
use uuid::Uuid;
|
|
13
|
-
|
|
14
|
-
/// Database error types
|
|
15
|
-
#[derive(Error, Debug)]
|
|
16
|
-
pub enum DbError {
|
|
17
|
-
#[error("SQLite error: {0}")]
|
|
18
|
-
Sqlite(#[from] rusqlite::Error),
|
|
19
|
-
#[error("Project not found: {0}")]
|
|
20
|
-
ProjectNotFound(String),
|
|
21
|
-
#[error("Tag not found: {0}")]
|
|
22
|
-
TagNotFound(String),
|
|
23
|
-
#[error("Database path error")]
|
|
24
|
-
PathError,
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
impl Serialize for DbError {
|
|
28
|
-
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
29
|
-
where
|
|
30
|
-
S: serde::Serializer,
|
|
31
|
-
{
|
|
32
|
-
serializer.serialize_str(&self.to_string())
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/// A project stored in the local database
|
|
37
|
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
38
|
-
#[serde(rename_all = "camelCase")]
|
|
39
|
-
pub struct Project {
|
|
40
|
-
pub id: String,
|
|
41
|
-
pub name: String,
|
|
42
|
-
pub path: String,
|
|
43
|
-
pub last_opened: String,
|
|
44
|
-
pub created_at: String,
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/// A project with its associated tags
|
|
48
|
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
49
|
-
#[serde(rename_all = "camelCase")]
|
|
50
|
-
pub struct ProjectWithTags {
|
|
51
|
-
pub id: String,
|
|
52
|
-
pub name: String,
|
|
53
|
-
pub path: String,
|
|
54
|
-
pub tags: Vec<Tag>,
|
|
55
|
-
pub last_opened: String,
|
|
56
|
-
pub created_at: String,
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/// A tag stored in the local database
|
|
60
|
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
61
|
-
pub struct Tag {
|
|
62
|
-
pub id: String,
|
|
63
|
-
pub name: String,
|
|
64
|
-
pub color: String,
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/// Input for creating a new project
|
|
68
|
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
69
|
-
pub struct CreateProjectInput {
|
|
70
|
-
pub name: String,
|
|
71
|
-
pub path: String,
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/// Input for updating a project
|
|
75
|
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
76
|
-
pub struct UpdateProjectInput {
|
|
77
|
-
pub name: Option<String>,
|
|
78
|
-
pub path: Option<String>,
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/// Input for creating a new tag
|
|
82
|
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
83
|
-
pub struct CreateTagInput {
|
|
84
|
-
pub name: String,
|
|
85
|
-
pub color: String,
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/// Input for adding a tag to a project
|
|
89
|
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
90
|
-
#[serde(rename_all = "camelCase")]
|
|
91
|
-
pub struct ProjectTagInput {
|
|
92
|
-
pub project_id: String,
|
|
93
|
-
pub tag_id: String,
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/// Thread-safe database wrapper
|
|
97
|
-
pub struct Database {
|
|
98
|
-
conn: Mutex<Connection>,
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
impl Database {
|
|
102
|
-
/// Creates a new database connection and initializes the schema
|
|
103
|
-
///
|
|
104
|
-
/// # Errors
|
|
105
|
-
///
|
|
106
|
-
/// Returns an error if the database cannot be opened or schema creation fails
|
|
107
|
-
pub fn new() -> Result<Self, DbError> {
|
|
108
|
-
let db_path = Self::get_db_path()?;
|
|
109
|
-
|
|
110
|
-
// Ensure parent directory exists
|
|
111
|
-
if let Some(parent) = db_path.parent() {
|
|
112
|
-
std::fs::create_dir_all(parent).map_err(|_| DbError::PathError)?;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
let conn = Connection::open(&db_path)?;
|
|
116
|
-
let db = Self {
|
|
117
|
-
conn: Mutex::new(conn),
|
|
118
|
-
};
|
|
119
|
-
db.init_schema()?;
|
|
120
|
-
Ok(db)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/// Creates an in-memory database for testing
|
|
124
|
-
#[cfg(test)]
|
|
125
|
-
pub fn new_in_memory() -> Result<Self, DbError> {
|
|
126
|
-
let conn = Connection::open_in_memory()?;
|
|
127
|
-
let db = Self {
|
|
128
|
-
conn: Mutex::new(conn),
|
|
129
|
-
};
|
|
130
|
-
db.init_schema()?;
|
|
131
|
-
Ok(db)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/// Gets the database file path in the app data directory
|
|
135
|
-
fn get_db_path() -> Result<PathBuf, DbError> {
|
|
136
|
-
let proj_dirs =
|
|
137
|
-
directories::ProjectDirs::from("com", "beads", "kanban-ui").ok_or(DbError::PathError)?;
|
|
138
|
-
Ok(proj_dirs.data_dir().join("settings.db"))
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/// Initializes the database schema
|
|
142
|
-
fn init_schema(&self) -> Result<(), DbError> {
|
|
143
|
-
let conn = self.conn.lock().unwrap();
|
|
144
|
-
|
|
145
|
-
conn.execute_batch(
|
|
146
|
-
r"
|
|
147
|
-
CREATE TABLE IF NOT EXISTS projects (
|
|
148
|
-
id TEXT PRIMARY KEY,
|
|
149
|
-
name TEXT NOT NULL,
|
|
150
|
-
path TEXT NOT NULL UNIQUE,
|
|
151
|
-
last_opened TEXT NOT NULL,
|
|
152
|
-
created_at TEXT NOT NULL
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
CREATE TABLE IF NOT EXISTS tags (
|
|
156
|
-
id TEXT PRIMARY KEY,
|
|
157
|
-
name TEXT NOT NULL,
|
|
158
|
-
color TEXT NOT NULL
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
CREATE TABLE IF NOT EXISTS project_tags (
|
|
162
|
-
project_id TEXT NOT NULL,
|
|
163
|
-
tag_id TEXT NOT NULL,
|
|
164
|
-
PRIMARY KEY (project_id, tag_id),
|
|
165
|
-
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
|
|
166
|
-
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
|
|
167
|
-
);
|
|
168
|
-
|
|
169
|
-
CREATE INDEX IF NOT EXISTS idx_projects_last_opened ON projects(last_opened DESC);
|
|
170
|
-
CREATE INDEX IF NOT EXISTS idx_project_tags_project ON project_tags(project_id);
|
|
171
|
-
CREATE INDEX IF NOT EXISTS idx_project_tags_tag ON project_tags(tag_id);
|
|
172
|
-
",
|
|
173
|
-
)?;
|
|
174
|
-
|
|
175
|
-
Ok(())
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// ===== Project CRUD =====
|
|
179
|
-
|
|
180
|
-
/// Gets all projects with their tags, ordered by last opened
|
|
181
|
-
pub fn get_projects_with_tags(&self) -> Result<Vec<ProjectWithTags>, DbError> {
|
|
182
|
-
let projects = self.get_projects()?;
|
|
183
|
-
let mut result = Vec::with_capacity(projects.len());
|
|
184
|
-
|
|
185
|
-
for project in projects {
|
|
186
|
-
let tags = self.get_project_tags(&project.id)?;
|
|
187
|
-
result.push(ProjectWithTags {
|
|
188
|
-
id: project.id,
|
|
189
|
-
name: project.name,
|
|
190
|
-
path: project.path,
|
|
191
|
-
tags,
|
|
192
|
-
last_opened: project.last_opened,
|
|
193
|
-
created_at: project.created_at,
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
Ok(result)
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/// Gets all projects, ordered by last opened
|
|
201
|
-
pub fn get_projects(&self) -> Result<Vec<Project>, DbError> {
|
|
202
|
-
let conn = self.conn.lock().unwrap();
|
|
203
|
-
let mut stmt = conn.prepare(
|
|
204
|
-
"SELECT id, name, path, last_opened, created_at FROM projects ORDER BY last_opened DESC",
|
|
205
|
-
)?;
|
|
206
|
-
|
|
207
|
-
let projects = stmt
|
|
208
|
-
.query_map([], |row| {
|
|
209
|
-
Ok(Project {
|
|
210
|
-
id: row.get(0)?,
|
|
211
|
-
name: row.get(1)?,
|
|
212
|
-
path: row.get(2)?,
|
|
213
|
-
last_opened: row.get(3)?,
|
|
214
|
-
created_at: row.get(4)?,
|
|
215
|
-
})
|
|
216
|
-
})?
|
|
217
|
-
.collect::<SqliteResult<Vec<_>>>()?;
|
|
218
|
-
|
|
219
|
-
Ok(projects)
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/// Creates a new project
|
|
223
|
-
pub fn create_project(&self, input: CreateProjectInput) -> Result<Project, DbError> {
|
|
224
|
-
let id = Uuid::new_v4().to_string();
|
|
225
|
-
let now = Utc::now().to_rfc3339();
|
|
226
|
-
|
|
227
|
-
let conn = self.conn.lock().unwrap();
|
|
228
|
-
conn.execute(
|
|
229
|
-
"INSERT INTO projects (id, name, path, last_opened, created_at) VALUES (?1, ?2, ?3, ?4, ?5)",
|
|
230
|
-
params![id, input.name, input.path, now, now],
|
|
231
|
-
)?;
|
|
232
|
-
|
|
233
|
-
Ok(Project {
|
|
234
|
-
id,
|
|
235
|
-
name: input.name,
|
|
236
|
-
path: input.path,
|
|
237
|
-
last_opened: now.clone(),
|
|
238
|
-
created_at: now,
|
|
239
|
-
})
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/// Updates an existing project
|
|
243
|
-
pub fn update_project(&self, id: &str, input: UpdateProjectInput) -> Result<Project, DbError> {
|
|
244
|
-
let conn = self.conn.lock().unwrap();
|
|
245
|
-
|
|
246
|
-
// Check if project exists
|
|
247
|
-
let exists: bool = conn
|
|
248
|
-
.query_row("SELECT 1 FROM projects WHERE id = ?1", params![id], |_| {
|
|
249
|
-
Ok(true)
|
|
250
|
-
})
|
|
251
|
-
.unwrap_or(false);
|
|
252
|
-
|
|
253
|
-
if !exists {
|
|
254
|
-
return Err(DbError::ProjectNotFound(id.to_string()));
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Update fields if provided
|
|
258
|
-
if let Some(ref name) = input.name {
|
|
259
|
-
conn.execute(
|
|
260
|
-
"UPDATE projects SET name = ?1 WHERE id = ?2",
|
|
261
|
-
params![name, id],
|
|
262
|
-
)?;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if let Some(ref path) = input.path {
|
|
266
|
-
conn.execute(
|
|
267
|
-
"UPDATE projects SET path = ?1 WHERE id = ?2",
|
|
268
|
-
params![path, id],
|
|
269
|
-
)?;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// Update last_opened
|
|
273
|
-
let now = Utc::now().to_rfc3339();
|
|
274
|
-
conn.execute(
|
|
275
|
-
"UPDATE projects SET last_opened = ?1 WHERE id = ?2",
|
|
276
|
-
params![now, id],
|
|
277
|
-
)?;
|
|
278
|
-
|
|
279
|
-
// Fetch and return updated project
|
|
280
|
-
let project = conn.query_row(
|
|
281
|
-
"SELECT id, name, path, last_opened, created_at FROM projects WHERE id = ?1",
|
|
282
|
-
params![id],
|
|
283
|
-
|row| {
|
|
284
|
-
Ok(Project {
|
|
285
|
-
id: row.get(0)?,
|
|
286
|
-
name: row.get(1)?,
|
|
287
|
-
path: row.get(2)?,
|
|
288
|
-
last_opened: row.get(3)?,
|
|
289
|
-
created_at: row.get(4)?,
|
|
290
|
-
})
|
|
291
|
-
},
|
|
292
|
-
)?;
|
|
293
|
-
|
|
294
|
-
Ok(project)
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/// Deletes a project by ID
|
|
298
|
-
pub fn delete_project(&self, id: &str) -> Result<(), DbError> {
|
|
299
|
-
let conn = self.conn.lock().unwrap();
|
|
300
|
-
let rows = conn.execute("DELETE FROM projects WHERE id = ?1", params![id])?;
|
|
301
|
-
|
|
302
|
-
if rows == 0 {
|
|
303
|
-
return Err(DbError::ProjectNotFound(id.to_string()));
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
Ok(())
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// ===== Tag CRUD =====
|
|
310
|
-
|
|
311
|
-
/// Gets all tags
|
|
312
|
-
pub fn get_tags(&self) -> Result<Vec<Tag>, DbError> {
|
|
313
|
-
let conn = self.conn.lock().unwrap();
|
|
314
|
-
let mut stmt = conn.prepare("SELECT id, name, color FROM tags ORDER BY name")?;
|
|
315
|
-
|
|
316
|
-
let tags = stmt
|
|
317
|
-
.query_map([], |row| {
|
|
318
|
-
Ok(Tag {
|
|
319
|
-
id: row.get(0)?,
|
|
320
|
-
name: row.get(1)?,
|
|
321
|
-
color: row.get(2)?,
|
|
322
|
-
})
|
|
323
|
-
})?
|
|
324
|
-
.collect::<SqliteResult<Vec<_>>>()?;
|
|
325
|
-
|
|
326
|
-
Ok(tags)
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/// Creates a new tag
|
|
330
|
-
pub fn create_tag(&self, input: CreateTagInput) -> Result<Tag, DbError> {
|
|
331
|
-
let id = Uuid::new_v4().to_string();
|
|
332
|
-
|
|
333
|
-
let conn = self.conn.lock().unwrap();
|
|
334
|
-
conn.execute(
|
|
335
|
-
"INSERT INTO tags (id, name, color) VALUES (?1, ?2, ?3)",
|
|
336
|
-
params![id, input.name, input.color],
|
|
337
|
-
)?;
|
|
338
|
-
|
|
339
|
-
Ok(Tag {
|
|
340
|
-
id,
|
|
341
|
-
name: input.name,
|
|
342
|
-
color: input.color,
|
|
343
|
-
})
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/// Deletes a tag by ID
|
|
347
|
-
pub fn delete_tag(&self, id: &str) -> Result<(), DbError> {
|
|
348
|
-
let conn = self.conn.lock().unwrap();
|
|
349
|
-
let rows = conn.execute("DELETE FROM tags WHERE id = ?1", params![id])?;
|
|
350
|
-
|
|
351
|
-
if rows == 0 {
|
|
352
|
-
return Err(DbError::TagNotFound(id.to_string()));
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
Ok(())
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// ===== Project-Tag Relationships =====
|
|
359
|
-
|
|
360
|
-
/// Gets all tags for a project
|
|
361
|
-
pub fn get_project_tags(&self, project_id: &str) -> Result<Vec<Tag>, DbError> {
|
|
362
|
-
let conn = self.conn.lock().unwrap();
|
|
363
|
-
let mut stmt = conn.prepare(
|
|
364
|
-
"SELECT t.id, t.name, t.color FROM tags t
|
|
365
|
-
INNER JOIN project_tags pt ON t.id = pt.tag_id
|
|
366
|
-
WHERE pt.project_id = ?1
|
|
367
|
-
ORDER BY t.name",
|
|
368
|
-
)?;
|
|
369
|
-
|
|
370
|
-
let tags = stmt
|
|
371
|
-
.query_map(params![project_id], |row| {
|
|
372
|
-
Ok(Tag {
|
|
373
|
-
id: row.get(0)?,
|
|
374
|
-
name: row.get(1)?,
|
|
375
|
-
color: row.get(2)?,
|
|
376
|
-
})
|
|
377
|
-
})?
|
|
378
|
-
.collect::<SqliteResult<Vec<_>>>()?;
|
|
379
|
-
|
|
380
|
-
Ok(tags)
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
/// Adds a tag to a project
|
|
384
|
-
pub fn add_tag_to_project(&self, project_id: &str, tag_id: &str) -> Result<(), DbError> {
|
|
385
|
-
let conn = self.conn.lock().unwrap();
|
|
386
|
-
|
|
387
|
-
// Verify project exists
|
|
388
|
-
let project_exists: bool = conn
|
|
389
|
-
.query_row(
|
|
390
|
-
"SELECT 1 FROM projects WHERE id = ?1",
|
|
391
|
-
params![project_id],
|
|
392
|
-
|_| Ok(true),
|
|
393
|
-
)
|
|
394
|
-
.unwrap_or(false);
|
|
395
|
-
|
|
396
|
-
if !project_exists {
|
|
397
|
-
return Err(DbError::ProjectNotFound(project_id.to_string()));
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// Verify tag exists
|
|
401
|
-
let tag_exists: bool = conn
|
|
402
|
-
.query_row("SELECT 1 FROM tags WHERE id = ?1", params![tag_id], |_| {
|
|
403
|
-
Ok(true)
|
|
404
|
-
})
|
|
405
|
-
.unwrap_or(false);
|
|
406
|
-
|
|
407
|
-
if !tag_exists {
|
|
408
|
-
return Err(DbError::TagNotFound(tag_id.to_string()));
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// Insert relationship (ignore if already exists)
|
|
412
|
-
conn.execute(
|
|
413
|
-
"INSERT OR IGNORE INTO project_tags (project_id, tag_id) VALUES (?1, ?2)",
|
|
414
|
-
params![project_id, tag_id],
|
|
415
|
-
)?;
|
|
416
|
-
|
|
417
|
-
Ok(())
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
/// Removes a tag from a project
|
|
421
|
-
pub fn remove_tag_from_project(&self, project_id: &str, tag_id: &str) -> Result<(), DbError> {
|
|
422
|
-
let conn = self.conn.lock().unwrap();
|
|
423
|
-
conn.execute(
|
|
424
|
-
"DELETE FROM project_tags WHERE project_id = ?1 AND tag_id = ?2",
|
|
425
|
-
params![project_id, tag_id],
|
|
426
|
-
)?;
|
|
427
|
-
|
|
428
|
-
Ok(())
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
#[cfg(test)]
|
|
433
|
-
mod tests {
|
|
434
|
-
use super::*;
|
|
435
|
-
|
|
436
|
-
#[test]
|
|
437
|
-
fn test_create_and_get_project() {
|
|
438
|
-
let db = Database::new_in_memory().unwrap();
|
|
439
|
-
|
|
440
|
-
let project = db
|
|
441
|
-
.create_project(CreateProjectInput {
|
|
442
|
-
name: "Test Project".to_string(),
|
|
443
|
-
path: "/path/to/project".to_string(),
|
|
444
|
-
})
|
|
445
|
-
.unwrap();
|
|
446
|
-
|
|
447
|
-
assert_eq!(project.name, "Test Project");
|
|
448
|
-
assert_eq!(project.path, "/path/to/project");
|
|
449
|
-
|
|
450
|
-
let projects = db.get_projects().unwrap();
|
|
451
|
-
assert_eq!(projects.len(), 1);
|
|
452
|
-
assert_eq!(projects[0].id, project.id);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
#[test]
|
|
456
|
-
fn test_update_project() {
|
|
457
|
-
let db = Database::new_in_memory().unwrap();
|
|
458
|
-
|
|
459
|
-
let project = db
|
|
460
|
-
.create_project(CreateProjectInput {
|
|
461
|
-
name: "Original".to_string(),
|
|
462
|
-
path: "/path".to_string(),
|
|
463
|
-
})
|
|
464
|
-
.unwrap();
|
|
465
|
-
|
|
466
|
-
let updated = db
|
|
467
|
-
.update_project(
|
|
468
|
-
&project.id,
|
|
469
|
-
UpdateProjectInput {
|
|
470
|
-
name: Some("Updated".to_string()),
|
|
471
|
-
path: None,
|
|
472
|
-
},
|
|
473
|
-
)
|
|
474
|
-
.unwrap();
|
|
475
|
-
|
|
476
|
-
assert_eq!(updated.name, "Updated");
|
|
477
|
-
assert_eq!(updated.path, "/path");
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
#[test]
|
|
481
|
-
fn test_delete_project() {
|
|
482
|
-
let db = Database::new_in_memory().unwrap();
|
|
483
|
-
|
|
484
|
-
let project = db
|
|
485
|
-
.create_project(CreateProjectInput {
|
|
486
|
-
name: "To Delete".to_string(),
|
|
487
|
-
path: "/delete/me".to_string(),
|
|
488
|
-
})
|
|
489
|
-
.unwrap();
|
|
490
|
-
|
|
491
|
-
db.delete_project(&project.id).unwrap();
|
|
492
|
-
|
|
493
|
-
let projects = db.get_projects().unwrap();
|
|
494
|
-
assert!(projects.is_empty());
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
#[test]
|
|
498
|
-
fn test_create_and_get_tag() {
|
|
499
|
-
let db = Database::new_in_memory().unwrap();
|
|
500
|
-
|
|
501
|
-
let tag = db
|
|
502
|
-
.create_tag(CreateTagInput {
|
|
503
|
-
name: "Frontend".to_string(),
|
|
504
|
-
color: "#3b82f6".to_string(),
|
|
505
|
-
})
|
|
506
|
-
.unwrap();
|
|
507
|
-
|
|
508
|
-
assert_eq!(tag.name, "Frontend");
|
|
509
|
-
assert_eq!(tag.color, "#3b82f6");
|
|
510
|
-
|
|
511
|
-
let tags = db.get_tags().unwrap();
|
|
512
|
-
assert_eq!(tags.len(), 1);
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
#[test]
|
|
516
|
-
fn test_project_tag_relationship() {
|
|
517
|
-
let db = Database::new_in_memory().unwrap();
|
|
518
|
-
|
|
519
|
-
let project = db
|
|
520
|
-
.create_project(CreateProjectInput {
|
|
521
|
-
name: "Project".to_string(),
|
|
522
|
-
path: "/project".to_string(),
|
|
523
|
-
})
|
|
524
|
-
.unwrap();
|
|
525
|
-
|
|
526
|
-
let tag = db
|
|
527
|
-
.create_tag(CreateTagInput {
|
|
528
|
-
name: "Urgent".to_string(),
|
|
529
|
-
color: "#ef4444".to_string(),
|
|
530
|
-
})
|
|
531
|
-
.unwrap();
|
|
532
|
-
|
|
533
|
-
db.add_tag_to_project(&project.id, &tag.id).unwrap();
|
|
534
|
-
|
|
535
|
-
let project_tags = db.get_project_tags(&project.id).unwrap();
|
|
536
|
-
assert_eq!(project_tags.len(), 1);
|
|
537
|
-
assert_eq!(project_tags[0].id, tag.id);
|
|
538
|
-
|
|
539
|
-
db.remove_tag_from_project(&project.id, &tag.id).unwrap();
|
|
540
|
-
|
|
541
|
-
let project_tags = db.get_project_tags(&project.id).unwrap();
|
|
542
|
-
assert!(project_tags.is_empty());
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
#[test]
|
|
546
|
-
fn test_get_projects_with_tags() {
|
|
547
|
-
let db = Database::new_in_memory().unwrap();
|
|
548
|
-
|
|
549
|
-
let project = db
|
|
550
|
-
.create_project(CreateProjectInput {
|
|
551
|
-
name: "Test".to_string(),
|
|
552
|
-
path: "/test".to_string(),
|
|
553
|
-
})
|
|
554
|
-
.unwrap();
|
|
555
|
-
|
|
556
|
-
let tag = db
|
|
557
|
-
.create_tag(CreateTagInput {
|
|
558
|
-
name: "Tag1".to_string(),
|
|
559
|
-
color: "#000".to_string(),
|
|
560
|
-
})
|
|
561
|
-
.unwrap();
|
|
562
|
-
|
|
563
|
-
db.add_tag_to_project(&project.id, &tag.id).unwrap();
|
|
564
|
-
|
|
565
|
-
let projects = db.get_projects_with_tags().unwrap();
|
|
566
|
-
assert_eq!(projects.len(), 1);
|
|
567
|
-
assert_eq!(projects[0].tags.len(), 1);
|
|
568
|
-
assert_eq!(projects[0].tags[0].name, "Tag1");
|
|
569
|
-
}
|
|
570
|
-
}
|