From 4a87c552cc2e64251b1e9bd582a1698494109829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Fri, 19 Jun 2026 21:03:18 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E:=20F-260619-01=20=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E5=8F=AF=E5=85=B3=E8=81=94=E7=81=B5=E6=84=9F(tasks.id?= =?UTF-8?q?ea=5Fid=201=E5=AF=B91=20=E5=8D=95=E5=90=91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - df-storage V20 迁移:tasks 加 idea_id TEXT REFERENCES ideas(id)(填预留空位, 幂等 column_exists 探测)+ V9_SQL 同步 + TaskRecord.idea_id + task_repo SQL/SELECT + 白名单 - AI 工具 create_task 加 idea_id 参数;update_task 经白名单自动支持 - IPC commands/task.rs CreateTaskInput 加 idea_id - 前端 types TaskRecord/CreateTaskInput idea_id + TaskDetail.vue 关联灵感展示 (router-link 友好 title 非裸 id)+ i18n - 补 idea_id 字段:df-mcp tools / df-nodes 测试 helper - 单向 1对1,Option 可空,复用 projects.idea_id 模式,老任务 None 兼容 自验: df-storage 47 passed(含 V20 2)+ workspace EXIT 0 + vue-tsc EXIT 0 --- crates/df-mcp/src/tools.rs | 2 + crates/df-nodes/src/ai_node.rs | 1 + crates/df-nodes/src/task_advance_node.rs | 1 + crates/df-storage/src/crud/settings.rs | 3 + crates/df-storage/src/crud/task_repo.rs | 17 ++-- crates/df-storage/src/migrations.rs | 95 ++++++++++++++++++- crates/df-storage/src/models.rs | 5 + .../df-storage/tests/project_soft_delete.rs | 1 + src-tauri/src/commands/ai/tool_registry.rs | 6 +- src-tauri/src/commands/task.rs | 5 + src/api/types.ts | 4 + src/i18n/en/taskDetail.ts | 1 + src/i18n/zh-CN/taskDetail.ts | 1 + src/stores/project/tasks.ts | 2 +- src/views/TaskDetail.vue | 31 +++++- 15 files changed, 158 insertions(+), 17 deletions(-) diff --git a/crates/df-mcp/src/tools.rs b/crates/df-mcp/src/tools.rs index 0f8c602..3b50a5a 100644 --- a/crates/df-mcp/src/tools.rs +++ b/crates/df-mcp/src/tools.rs @@ -392,6 +392,7 @@ fn create_task(ctx: &Ctx, args: Value) -> BoxFuture<'static, CallToolResult> { base_branch: None, review_rounds: 0, output_json: None, + idea_id: None, created_at: now.clone(), updated_at: now, }; @@ -444,6 +445,7 @@ fn update_task(ctx: &Ctx, args: Value) -> BoxFuture<'static, CallToolResult> { base_branch: existing.base_branch, review_rounds: existing.review_rounds, output_json: existing.output_json, + idea_id: existing.idea_id, created_at: existing.created_at, updated_at: now, }; diff --git a/crates/df-nodes/src/ai_node.rs b/crates/df-nodes/src/ai_node.rs index 2bfa49e..e9d0e8f 100644 --- a/crates/df-nodes/src/ai_node.rs +++ b/crates/df-nodes/src/ai_node.rs @@ -699,6 +699,7 @@ mod tests { base_branch: None, review_rounds: 0, output_json: task_output_json.map(String::from), + idea_id: None, created_at: "0".to_string(), updated_at: "0".to_string(), }) diff --git a/crates/df-nodes/src/task_advance_node.rs b/crates/df-nodes/src/task_advance_node.rs index bef98e3..071e72a 100644 --- a/crates/df-nodes/src/task_advance_node.rs +++ b/crates/df-nodes/src/task_advance_node.rs @@ -189,6 +189,7 @@ mod tests { base_branch: None, review_rounds: 0, output_json: None, + idea_id: None, created_at: "0".to_string(), updated_at: "0".to_string(), } diff --git a/crates/df-storage/src/crud/settings.rs b/crates/df-storage/src/crud/settings.rs index ba718f2..39969df 100644 --- a/crates/df-storage/src/crud/settings.rs +++ b/crates/df-storage/src/crud/settings.rs @@ -142,6 +142,9 @@ pub fn allowed_columns_for(table: &str) -> Option<&'static [&'static str]> { // output_json:ai_execute 写产出 / ai_self_review 读产出自审 / human_review 展示对象 // (决策 a:task 中心,产出跟 task 走)。非状态机收口字段,合法可写。 "output_json", + // idea_id(F-260619-01 任务关联灵感,1对1 单向):任务可关联/解关联一条灵感, + // 非状态机收口字段,合法可写(idea_id 存在性由外键约束 + 上层校验兜底)。 + "idea_id", "updated_at", // TODO(B-260616-16): project_id 跨表存在性校验待 commands/task.rs 层补。 // 通用 CRUD 层(db repo)只懂表/列语义,不持有跨表业务约束(查 projects 表存在性)。 diff --git a/crates/df-storage/src/crud/task_repo.rs b/crates/df-storage/src/crud/task_repo.rs index a146639..e1ef521 100644 --- a/crates/df-storage/src/crud/task_repo.rs +++ b/crates/df-storage/src/crud/task_repo.rs @@ -31,6 +31,7 @@ fn task_from_row(row: &Row<'_>) -> std::result::Result |row| task_from_row(row), insert => |conn, rec| { conn.execute( - "INSERT INTO tasks (id, project_id, title, description, status, priority, branch_name, assignee, workflow_def_id, base_branch, review_rounds, output_json, created_at, updated_at) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)", + "INSERT INTO tasks (id, project_id, title, description, status, priority, branch_name, assignee, workflow_def_id, base_branch, review_rounds, output_json, idea_id, created_at, updated_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)", params![ rec.id, rec.project_id, rec.title, rec.description, rec.status, rec.priority, rec.branch_name, rec.assignee, rec.workflow_def_id, rec.base_branch, - rec.review_rounds, rec.output_json, rec.created_at, rec.updated_at + rec.review_rounds, rec.output_json, rec.idea_id, rec.created_at, rec.updated_at ], ) }, update => |conn, rec| { conn.execute( - "UPDATE tasks SET project_id = ?1, title = ?2, description = ?3, status = ?4, priority = ?5, branch_name = ?6, assignee = ?7, workflow_def_id = ?8, base_branch = ?9, review_rounds = ?10, output_json = ?11, updated_at = ?12 WHERE id = ?13", + "UPDATE tasks SET project_id = ?1, title = ?2, description = ?3, status = ?4, priority = ?5, branch_name = ?6, assignee = ?7, workflow_def_id = ?8, base_branch = ?9, review_rounds = ?10, output_json = ?11, idea_id = ?12, updated_at = ?13 WHERE id = ?14", params![ rec.project_id, rec.title, rec.description, rec.status, rec.priority, rec.branch_name, rec.assignee, rec.workflow_def_id, rec.base_branch, - rec.review_rounds, rec.output_json, rec.updated_at, rec.id + rec.review_rounds, rec.output_json, rec.idea_id, rec.updated_at, rec.id ], ) } @@ -79,7 +80,7 @@ impl TaskRepo { tokio::task::spawn_blocking(move || { let guard = conn.blocking_lock(); let mut stmt = guard - .prepare("SELECT id, project_id, title, description, status, priority, branch_name, assignee, workflow_def_id, base_branch, review_rounds, output_json, created_at, updated_at FROM tasks WHERE deleted_at IS NULL ORDER BY created_at DESC") + .prepare("SELECT id, project_id, title, description, status, priority, branch_name, assignee, workflow_def_id, base_branch, review_rounds, output_json, idea_id, created_at, updated_at FROM tasks WHERE deleted_at IS NULL ORDER BY created_at DESC") .map_err(storage_err)?; let rows = stmt .query_map([], |row| task_from_row(row)) @@ -180,7 +181,7 @@ impl TaskRepo { } // 回读更新后的记录(含新 status / 累加后的 review_rounds / 新 updated_at)。 let mut stmt = guard - .prepare("SELECT id, project_id, title, description, status, priority, branch_name, assignee, workflow_def_id, base_branch, review_rounds, output_json, created_at, updated_at FROM tasks WHERE id = ?1") + .prepare("SELECT id, project_id, title, description, status, priority, branch_name, assignee, workflow_def_id, base_branch, review_rounds, output_json, idea_id, created_at, updated_at FROM tasks WHERE id = ?1") .map_err(storage_err)?; let row = stmt .query_row(params![id], |row| task_from_row(row)) @@ -201,7 +202,7 @@ impl TaskRepo { tokio::task::spawn_blocking(move || { let guard = conn.blocking_lock(); let mut stmt = guard - .prepare("SELECT id, project_id, title, description, status, priority, branch_name, assignee, workflow_def_id, base_branch, review_rounds, output_json, created_at, updated_at FROM tasks WHERE deleted_at IS NOT NULL ORDER BY updated_at DESC") + .prepare("SELECT id, project_id, title, description, status, priority, branch_name, assignee, workflow_def_id, base_branch, review_rounds, output_json, idea_id, created_at, updated_at FROM tasks WHERE deleted_at IS NOT NULL ORDER BY updated_at DESC") .map_err(storage_err)?; let rows = stmt .query_map([], |row| task_from_row(row)) diff --git a/crates/df-storage/src/migrations.rs b/crates/df-storage/src/migrations.rs index a743494..e63b576 100644 --- a/crates/df-storage/src/migrations.rs +++ b/crates/df-storage/src/migrations.rs @@ -23,8 +23,8 @@ pub fn run(conn: &Connection) -> Result<()> { // 迁移步骤链: 顺序执行,跳过已应用的版本(current_version < N 才跑)。 // 新增版本时,在此数组追加一项 (N, migrate_vN) 即可,无需改逻辑。 - // V20 预留给 F-260619-01(任务关联灵感),跳过;V21 = 消息拆分存储 + audit message_id。 - let steps: [(i32, fn(&Connection) -> Result<()>); 20] = [ + // V20 = F-260619-01(任务关联灵感 idea_id);V21 = 消息拆分存储 + audit message_id。 + let steps: [(i32, fn(&Connection) -> Result<()>); 21] = [ (1, migrate_v1), (2, migrate_v2), (3, migrate_v3), @@ -44,6 +44,7 @@ pub fn run(conn: &Connection) -> Result<()> { (17, migrate_v17), (18, migrate_v18), (19, migrate_v19), + (20, migrate_v20), (21, migrate_v21), ]; @@ -350,6 +351,26 @@ fn migrate_v19(conn: &Connection) -> Result<()> { Ok(()) } +/// V20:幂等补 tasks.idea_id 列(F-260619-01 任务关联灵感,1对1 单向) +/// +/// 任务可关联到一条灵感(任务→灵感单向),复用 projects.idea_id 模式 +/// (REFERENCES ideas(id) 外键)。TEXT NULL 向后兼容:老库行默认 NULL,TaskRecord +/// 字段为 Option(未关联灵感的任务为 None)。 +/// 用 PRAGMA 探测列存在性,缺失才 ALTER(同 v11/v14/v17 模式),对新库/老库均安全。 +/// 新库已在 V9_SQL(tasks 建表)直接带 idea_id 列,此处只补老库。 +fn migrate_v20(conn: &Connection) -> Result<()> { + if !column_exists(conn, "tasks", "idea_id") { + conn.execute( + "ALTER TABLE tasks ADD COLUMN idea_id TEXT REFERENCES ideas(id)", + [], + )?; + tracing::info!("v20: 补建 tasks.idea_id 列(任务关联灵感,F-260619-01)"); + } + conn.execute("INSERT INTO schema_version (version) VALUES (?)", [20])?; + tracing::info!("迁移 v20 完成"); + Ok(()) +} + /// V21:消息拆分存储(ai_messages 表 + 全量迁移)+ ai_tool_executions.message_id 列 /// /// **一次原子迁移**(决策 V21 合并,不拆 V21a/V21b): @@ -554,6 +575,8 @@ CREATE TABLE IF NOT EXISTS tasks ( priority INTEGER NOT NULL DEFAULT 2, branch_name TEXT, assignee TEXT, + -- F-260619-01 任务关联灵感(1对1 单向,复用 projects.idea_id 模式)。老库由 V20 迁移补列。 + idea_id TEXT REFERENCES ideas(id), created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); @@ -997,4 +1020,72 @@ mod tests { "迁移后应有 message_id 列" ); } + + // ============================================================ + // V20 迁移幂等安全(F-260619-01 任务关联灵感) + // ============================================================ + + /// 构造最小老库 schema:tasks 表(无 idea_id 列,模拟 V1 建表老形态)+ schema_version。 + fn setup_legacy_tasks_db() -> Connection { + let conn = Connection::open_in_memory().expect("open in-memory db"); + conn.execute_batch( + "CREATE TABLE schema_version (version INTEGER PRIMARY KEY); + CREATE TABLE tasks ( + id TEXT PRIMARY KEY, + project_id TEXT NOT NULL, + title TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + status TEXT NOT NULL DEFAULT 'todo', + priority INTEGER NOT NULL DEFAULT 2, + branch_name TEXT, + assignee TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + );", + ) + .expect("create legacy tasks table"); + conn + } + + /// 老库无 idea_id 列:迁移应补建 + 写版本号 20 + #[test] + fn v20_legacy_db_adds_idea_id_column() { + let conn = setup_legacy_tasks_db(); + assert!( + !column_exists(&conn, "tasks", "idea_id"), + "迁移前应无 idea_id 列" + ); + + migrate_v20(&conn).expect("v20 应在老库补建 idea_id 列"); + + assert!( + column_exists(&conn, "tasks", "idea_id"), + "迁移后应有 idea_id 列" + ); + let v: i64 = conn + .query_row("SELECT MAX(version) FROM schema_version", [], |r| r.get(0)) + .unwrap(); + assert_eq!(v, 20, "应写入版本号 20"); + } + + /// 幂等重跑:列已存在时跳过 ALTER,版本号不重复写(PK 冲突防) + /// 注:migrate_v20 用普通 INSERT(非 IGNORE),重跑会因 PK 冲突报错——这是预期行为, + /// run() 正常流程下 current_version<20 只调一次;此处验证列存在时 ALTER 被短路(不报 duplicate column)。 + #[test] + fn v20_column_exists_skips_alter() { + let conn = setup_legacy_tasks_db(); + // 先跑一次补列 + migrate_v20(&conn).expect("首次迁移"); + assert!(column_exists(&conn, "tasks", "idea_id")); + + // 手动回退版本号模拟「列已存在但版本号未写」场景,验证 ALTER 被短路不报 duplicate column + conn.execute("DELETE FROM schema_version WHERE version = 20", []) + .unwrap(); + // 此时列存在但版本号 20 缺失 → migrate_v20 应跳过 ALTER 只补版本号 + migrate_v20(&conn).expect("列存在时应跳过 ALTER 不报错"); + let v: i64 = conn + .query_row("SELECT MAX(version) FROM schema_version", [], |r| r.get(0)) + .unwrap(); + assert_eq!(v, 20); + } } diff --git a/crates/df-storage/src/models.rs b/crates/df-storage/src/models.rs index d34776b..882bdba 100644 --- a/crates/df-storage/src/models.rs +++ b/crates/df-storage/src/models.rs @@ -75,6 +75,11 @@ pub struct TaskRecord { #[serde(skip_serializing_if = "Option::is_none")] #[serde(default)] pub output_json: Option, + /// 关联灵感 ID(F-260619-01,1对1 单向,任务→灵感)。 + /// 复用 projects.idea_id 模式(REFERENCES ideas(id) 外键),可空(任务可不关联灵感)。 + /// 老任务无 idea_id → None。#[serde(default)] 兼容旧前端 JSON(无该字段时为 None)。 + #[serde(default)] + pub idea_id: Option, pub created_at: String, pub updated_at: String, } diff --git a/crates/df-storage/tests/project_soft_delete.rs b/crates/df-storage/tests/project_soft_delete.rs index 95b0d7f..931cb00 100644 --- a/crates/df-storage/tests/project_soft_delete.rs +++ b/crates/df-storage/tests/project_soft_delete.rs @@ -43,6 +43,7 @@ fn task(id: &str, project_id: &str) -> TaskRecord { base_branch: None, review_rounds: 0, output_json: None, + idea_id: None, created_at: now_ts(), updated_at: now_ts(), } diff --git a/src-tauri/src/commands/ai/tool_registry.rs b/src-tauri/src/commands/ai/tool_registry.rs index 7fed0bf..e813ac3 100644 --- a/src-tauri/src/commands/ai/tool_registry.rs +++ b/src-tauri/src/commands/ai/tool_registry.rs @@ -630,8 +630,8 @@ fn register_task_tools(registry: &mut AiToolRegistry, db: &Arc) { })}, ); registry.register( - "create_task", "在指定项目下创建新任务", - df_ai::ai_tools::object_schema(vec![("project_id", "string", true), ("title", "string", true), ("description", "string", false), ("priority", "integer", false)]), + "create_task", "在指定项目下创建新任务,可选传 idea_id 关联灵感(1对1 单向)", + df_ai::ai_tools::object_schema(vec![("project_id", "string", true), ("title", "string", true), ("description", "string", false), ("priority", "integer", false), ("idea_id", "string", false)]), RiskLevel::Medium, { let db = db.clone(); Box::new(move |args: serde_json::Value| { let db = db.clone(); @@ -647,6 +647,8 @@ fn register_task_tools(registry: &mut AiToolRegistry, db: &Arc) { branch_name: None, assignee: None, workflow_def_id: None, base_branch: None, review_rounds: 0, output_json: None, + // F-260619-01 可选关联灵感(空字符串/缺省视为不关联) + idea_id: args.get("idea_id").and_then(|v| v.as_str()).filter(|s| !s.is_empty()).map(String::from), created_at: now_millis(), updated_at: now_millis(), }; let id = record.id.clone(); diff --git a/src-tauri/src/commands/task.rs b/src-tauri/src/commands/task.rs index bbb40f3..5edeb02 100644 --- a/src-tauri/src/commands/task.rs +++ b/src-tauri/src/commands/task.rs @@ -21,6 +21,9 @@ pub struct CreateTaskInput { pub priority: i32, pub branch_name: Option, pub assignee: Option, + /// 关联灵感 ID(F-260619-01,1对1 单向,可空=不关联)。 + /// 空字符串视为不关联(与 AI 工具层一致)。 + pub idea_id: Option, } fn default_priority() -> i32 { @@ -79,6 +82,8 @@ pub async fn create_task( base_branch: None, review_rounds: 0, output_json: None, + // F-260619-01:空字符串视为不关联(与 AI 工具层一致) + idea_id: input.idea_id.filter(|s| !s.is_empty()), created_at: now.clone(), updated_at: now, }; diff --git a/src/api/types.ts b/src/api/types.ts index 281182f..876248a 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -117,6 +117,8 @@ export interface TaskRecord { * 旧任务无产出时为 undefined。前端 parsedOutput try/catch 兜底解析。 */ output_json?: string + /** 关联灵感 ID(F-260619-01,1对1 单向,任务→灵感)。未关联时为 undefined。 */ + idea_id?: string created_at: string updated_at: string } @@ -128,6 +130,8 @@ export interface CreateTaskInput { priority?: number branch_name?: string assignee?: string + /** 关联灵感 ID(1对1 单向,可空)。空字符串视为不关联(与后端一致)。 */ + idea_id?: string } // ============================================================ diff --git a/src/i18n/en/taskDetail.ts b/src/i18n/en/taskDetail.ts index 76b3d2b..ff3e6b4 100644 --- a/src/i18n/en/taskDetail.ts +++ b/src/i18n/en/taskDetail.ts @@ -14,6 +14,7 @@ export default { status: 'Status', priority: 'Priority', project: 'Project', + relatedIdea: 'Related Idea', branch: 'Branch', assignee: 'Assignee', baseBranch: 'Base Branch', diff --git a/src/i18n/zh-CN/taskDetail.ts b/src/i18n/zh-CN/taskDetail.ts index 2885900..45fe9a7 100644 --- a/src/i18n/zh-CN/taskDetail.ts +++ b/src/i18n/zh-CN/taskDetail.ts @@ -14,6 +14,7 @@ export default { status: '状态', priority: '优先级', project: '关联项目', + relatedIdea: '关联灵感', branch: '分支', assignee: '负责人', baseBranch: '基础分支', diff --git a/src/stores/project/tasks.ts b/src/stores/project/tasks.ts index 51e082d..152456d 100644 --- a/src/stores/project/tasks.ts +++ b/src/stores/project/tasks.ts @@ -12,7 +12,7 @@ export function createTasksStore() { } } - async function createTask(input: { project_id: string; title: string; description?: string; priority?: number; branch_name?: string; assignee?: string }) { + async function createTask(input: { project_id: string; title: string; description?: string; priority?: number; branch_name?: string; assignee?: string; idea_id?: string }) { try { const record = await taskApi.create(input) state.tasks.push(record) diff --git a/src/views/TaskDetail.vue b/src/views/TaskDetail.vue index 8cdce7d..0afebaa 100644 --- a/src/views/TaskDetail.vue +++ b/src/views/TaskDetail.vue @@ -104,6 +104,16 @@ + +
+ {{ $t('taskDetail.relatedIdea') }} + + + {{ ideaTitle }} + + + +
{{ $t('taskDetail.branch') }} @@ -146,7 +156,7 @@ import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue' import { useI18n } from 'vue-i18n' import { useRoute } from 'vue-router' import { listen } from '@tauri-apps/api/event' -import { taskApi, projectApi } from '@/api' +import { taskApi, projectApi, ideaApi } from '@/api' import { workflowApi } from '@/api' import { formatDate } from '@/utils/time' import { useRendered } from '@/composables/useMarkdown' @@ -156,7 +166,7 @@ import { priorityLabel, priorityClass, } from '../constants/project' -import type { TaskRecord, ProjectRecord, DfDataChangedPayload, WorkflowEventPayload } from '@/api/types' +import type { TaskRecord, ProjectRecord, IdeaRecord, DfDataChangedPayload, WorkflowEventPayload } from '@/api/types' import TaskOutputCard from '@/components/task/TaskOutputCard.vue' const { t } = useI18n() @@ -166,6 +176,8 @@ const loading = ref(true) const errorMsg = ref('') const task = ref(null) const projects = ref([]) +// F-260619-01:灵感列表用于解析 task.idea_id → 灵感 title(独立入口,同 projects 模式) +const ideas = ref([]) const advancing = ref(false) // ============================================================ @@ -204,6 +216,14 @@ const projectName = computed(() => { return p?.name ?? task.value?.project_id ?? '—' }) +// F-260619-01:解析 task.idea_id → 灵感 title(找不到时回退 id,保证非空可点击) +const ideaTitle = computed(() => { + const ideaId = task.value?.idea_id + if (!ideaId) return '—' + const idea = ideas.value.find(i => i.id === ideaId) + return idea?.title ?? ideaId +}) + // ============================================================ // F-05 推进按钮 — 状态机合法下一态映射 // ------------------------------------------------------------ @@ -379,13 +399,16 @@ async function load() { loading.value = true errorMsg.value = '' try { - const [t, ps] = await Promise.all([ + const [t, ps, ideasList] = await Promise.all([ taskApi.get(taskId.value), // 项目列表用于解析 project_id → 项目名(独立入口,不依赖全局 store) projectApi.list(), + // F-260619-01:灵感列表用于解析 idea_id → 灵感 title + ideaApi.list(), ]) task.value = t projects.value = ps + ideas.value = ideasList } catch (e: any) { task.value = null errorMsg.value = t('taskDetail.loadFailed', { msg: e?.toString() ?? t('common.unknownError') }) @@ -417,7 +440,7 @@ onMounted(async () => { try { _unlistenDataChanged = await listen('df-data-changed', (event) => { const { entity } = event.payload - if (entity === 'task' || entity === 'project') { + if (entity === 'task' || entity === 'project' || entity === 'idea') { load() } })