feat: 项目 logo + 侧栏拖动排序 + 合并修复(主检出干净时 in-place)
- 项目 logo:仓库内文件(logo/icon/favicon 等多路径)→git remote 头像→自定义(URL/相对路径)→ 首字母徽章兜底;GET /api/projects/:id/logo(文件流/302),配置面板加 Logo 输入 - 侧栏项目拖动排序:projects.sort_order + POST /api/projects/reorder,乐观更新 - 合并修复:默认分支正被主检出占用时,若工作区干净则直接在主检出 in-place 合并 (用户手动合并的等价操作,安全);脏工作区拒绝并提示提交/暂存或仅通过 - schema: projects.logo / sort_order(ensureColumn 平滑迁移) 测试 75/75(merge 用例改为干净→成功/脏→拒绝) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,8 @@ export function openDb(file: string): Database.Database {
|
||||
db.pragma('foreign_keys = ON');
|
||||
ensureColumn(db, 'tasks', 'source_ref', 'source_ref TEXT');
|
||||
ensureColumn(db, 'projects', 'last_sync_at', 'last_sync_at TEXT');
|
||||
ensureColumn(db, 'projects', 'logo', 'logo TEXT'); // 自定义 logo(URL/仓库内相对路径,null=自动解析)
|
||||
ensureColumn(db, 'projects', 'sort_order', 'sort_order INTEGER NOT NULL DEFAULT 0'); // 侧栏排序
|
||||
const schema = readFileSync(join(HERE, 'schema.sql'), 'utf8');
|
||||
db.exec(schema);
|
||||
return db;
|
||||
|
||||
@@ -8,7 +8,7 @@ export interface ProjectRow {
|
||||
id: string; name: string; repo_path: string; default_branch: string;
|
||||
verify_cmd: string | null; autonomy: string; model: string | null;
|
||||
concurrency: number; status: string; created_at: string;
|
||||
last_sync_at: string | null;
|
||||
last_sync_at: string | null; logo: string | null; sort_order: number;
|
||||
}
|
||||
export interface TaskRow {
|
||||
id: string; project_id: string; parent_id: string | null; depth: number;
|
||||
@@ -35,7 +35,7 @@ export function rowToProject(r: ProjectRow): Project {
|
||||
id: r.id, name: r.name, repoPath: r.repo_path, defaultBranch: r.default_branch,
|
||||
verifyCmd: r.verify_cmd, autonomy: r.autonomy as Autonomy, model: r.model,
|
||||
concurrency: r.concurrency, status: r.status as Project['status'], createdAt: r.created_at,
|
||||
lastSyncAt: r.last_sync_at,
|
||||
lastSyncAt: r.last_sync_at, logo: r.logo, sortOrder: r.sort_order,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,9 @@ CREATE TABLE IF NOT EXISTS projects (
|
||||
concurrency INTEGER NOT NULL DEFAULT 1,
|
||||
status TEXT NOT NULL DEFAULT 'active', -- active | paused
|
||||
created_at TEXT NOT NULL,
|
||||
last_sync_at TEXT -- 最近一次 todo.json 同步时间
|
||||
last_sync_at TEXT, -- 最近一次 todo.json 同步时间
|
||||
logo TEXT, -- 自定义 logo(URL / 仓库内相对路径;null=自动)
|
||||
sort_order INTEGER NOT NULL DEFAULT 0 -- 侧栏排序(小在前)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tasks (
|
||||
|
||||
+23
-5
@@ -27,7 +27,7 @@ export interface CreateTaskInput {
|
||||
}
|
||||
export interface PatchProjectInput {
|
||||
autonomy?: Autonomy; concurrency?: number; verifyCmd?: string | null;
|
||||
model?: string | null; status?: 'active' | 'paused';
|
||||
model?: string | null; status?: 'active' | 'paused'; logo?: string | null;
|
||||
}
|
||||
export interface PatchTaskInput {
|
||||
title?: string; priority?: number; complexity?: Complexity;
|
||||
@@ -89,26 +89,43 @@ export class Store {
|
||||
|
||||
// ---------- Projects ----------
|
||||
createProject(input: CreateProjectInput): Project {
|
||||
const maxOrder = (this.db.prepare(`SELECT COALESCE(MAX(sort_order), -1) AS m FROM projects`).get() as { m: number }).m;
|
||||
const row: ProjectRow = {
|
||||
id: id('prj'), name: input.name, repo_path: input.repoPath,
|
||||
default_branch: input.defaultBranch ?? 'main', verify_cmd: input.verifyCmd ?? null,
|
||||
autonomy: input.autonomy ?? 'manual', model: input.model ?? null,
|
||||
concurrency: input.concurrency ?? 1, status: 'active', created_at: now(),
|
||||
last_sync_at: null,
|
||||
last_sync_at: null, logo: null, sort_order: maxOrder + 1,
|
||||
};
|
||||
this.db.prepare(
|
||||
`INSERT INTO projects (id,name,repo_path,default_branch,verify_cmd,autonomy,model,concurrency,status,created_at,last_sync_at)
|
||||
VALUES (@id,@name,@repo_path,@default_branch,@verify_cmd,@autonomy,@model,@concurrency,@status,@created_at,@last_sync_at)`,
|
||||
`INSERT INTO projects (id,name,repo_path,default_branch,verify_cmd,autonomy,model,concurrency,status,created_at,last_sync_at,logo,sort_order)
|
||||
VALUES (@id,@name,@repo_path,@default_branch,@verify_cmd,@autonomy,@model,@concurrency,@status,@created_at,@last_sync_at,@logo,@sort_order)`,
|
||||
).run(row);
|
||||
this.emit(row.id, null, 'task.created', { kind: 'project', name: row.name });
|
||||
return rowToProject(row);
|
||||
}
|
||||
|
||||
listProjects(): Project[] {
|
||||
const rows = this.db.prepare(`SELECT * FROM projects ORDER BY created_at`).all() as ProjectRow[];
|
||||
const rows = this.db.prepare(`SELECT * FROM projects ORDER BY sort_order, created_at`).all() as ProjectRow[];
|
||||
return rows.map(rowToProject);
|
||||
}
|
||||
|
||||
/** 重排项目(侧栏拖动):按给定 id 顺序写 sort_order;未列出的排在后面、相对顺序不变。 */
|
||||
reorderProjects(orderedIds: string[]): Project[] {
|
||||
const txn = this.db.transaction(() => {
|
||||
let i = 0;
|
||||
const upd = this.db.prepare(`UPDATE projects SET sort_order = ? WHERE id = ?`);
|
||||
for (const pid of orderedIds) upd.run(i++, pid);
|
||||
// 未列出的项目顺延到末尾(保持原相对序)
|
||||
const rest = this.db.prepare(
|
||||
`SELECT id FROM projects WHERE id NOT IN (${orderedIds.map(() => '?').join(',') || 'NULL'}) ORDER BY sort_order, created_at`,
|
||||
).all(...orderedIds) as Array<{ id: string }>;
|
||||
for (const r of rest) upd.run(i++, r.id);
|
||||
});
|
||||
txn();
|
||||
return this.listProjects();
|
||||
}
|
||||
|
||||
getProject(projectId: string): Project | null {
|
||||
const row = this.db.prepare(`SELECT * FROM projects WHERE id = ?`).get(projectId) as ProjectRow | undefined;
|
||||
return row ? rowToProject(row) : null;
|
||||
@@ -141,6 +158,7 @@ export class Store {
|
||||
}
|
||||
if (patch.verifyCmd !== undefined) { sets.push('verify_cmd = @verify_cmd'); args.verify_cmd = patch.verifyCmd; }
|
||||
if (patch.model !== undefined) { sets.push('model = @model'); args.model = patch.model; }
|
||||
if (patch.logo !== undefined) { sets.push('logo = @logo'); args.logo = patch.logo; }
|
||||
|
||||
if (sets.length > 0) {
|
||||
this.db.prepare(`UPDATE projects SET ${sets.join(', ')} WHERE id = @id`).run(args);
|
||||
|
||||
Reference in New Issue
Block a user