@knotpad/app 0.1.0 → 0.1.1

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.
Files changed (341) hide show
  1. package/bin/brief.js +165 -78
  2. package/package.json +3 -17
  3. package/app/(app)/calendar/page.tsx +0 -57
  4. package/app/(app)/error.tsx +0 -35
  5. package/app/(app)/graph/page.tsx +0 -32
  6. package/app/(app)/guide/page.tsx +0 -21
  7. package/app/(app)/kanban/loading.tsx +0 -24
  8. package/app/(app)/kanban/page.tsx +0 -59
  9. package/app/(app)/layout.tsx +0 -122
  10. package/app/(app)/list/loading.tsx +0 -21
  11. package/app/(app)/list/page.tsx +0 -137
  12. package/app/(app)/loading.tsx +0 -18
  13. package/app/(app)/notes/[noteId]/page.tsx +0 -84
  14. package/app/(app)/notes/layout.tsx +0 -30
  15. package/app/(app)/notes/page.tsx +0 -39
  16. package/app/(app)/page.tsx +0 -5
  17. package/app/(app)/settings/agent-token/page.tsx +0 -59
  18. package/app/(app)/settings/backup/page.tsx +0 -49
  19. package/app/(app)/settings/billing/page.tsx +0 -53
  20. package/app/(app)/settings/calendar/page.tsx +0 -41
  21. package/app/(app)/settings/layout.test.tsx +0 -39
  22. package/app/(app)/settings/layout.tsx +0 -71
  23. package/app/(app)/settings/page.tsx +0 -4
  24. package/app/(app)/settings/security/page.tsx +0 -43
  25. package/app/(app)/settings/team/page.tsx +0 -74
  26. package/app/(app)/settings/workspace/page.tsx +0 -27
  27. package/app/(app)/tasks/[taskId]/page.tsx +0 -79
  28. package/app/(auth)/forgot-password/page.tsx +0 -106
  29. package/app/(auth)/guest/page.tsx +0 -56
  30. package/app/(auth)/layout.tsx +0 -13
  31. package/app/(auth)/login/page.tsx +0 -14
  32. package/app/(auth)/register/page.tsx +0 -193
  33. package/app/(auth)/reset-password/page.tsx +0 -138
  34. package/app/api/account/claim/route.tsx +0 -135
  35. package/app/api/admin/backfill-encryption/route.tsx +0 -43
  36. package/app/api/admin/license/route.tsx +0 -42
  37. package/app/api/auth/2fa/route.tsx +0 -148
  38. package/app/api/auth/[...nextauth]/route.tsx +0 -3
  39. package/app/api/auth/change-password/route.tsx +0 -61
  40. package/app/api/auth/check-2fa/route.tsx +0 -19
  41. package/app/api/auth/forgot-password/route.tsx +0 -65
  42. package/app/api/auth/reset-password/route.tsx +0 -52
  43. package/app/api/auth/verify-2fa/route.tsx +0 -88
  44. package/app/api/backup/download/db/route.ts +0 -29
  45. package/app/api/backup/download/notes/route.ts +0 -25
  46. package/app/api/backup/settings/route.ts +0 -92
  47. package/app/api/billing/checkout/route.tsx +0 -81
  48. package/app/api/billing/migrate/route.tsx +0 -163
  49. package/app/api/billing/portal/route.tsx +0 -24
  50. package/app/api/billing/setup-intent/route.tsx +0 -55
  51. package/app/api/billing/status/route.tsx +0 -36
  52. package/app/api/billing/subscribe/route.tsx +0 -85
  53. package/app/api/billing/webhook/route.tsx +0 -199
  54. package/app/api/calendar-feeds/[feedId]/route.tsx +0 -67
  55. package/app/api/calendar-feeds/[feedId]/sync/route.tsx +0 -37
  56. package/app/api/calendar-feeds/events/route.tsx +0 -82
  57. package/app/api/calendar-feeds/route.tsx +0 -52
  58. package/app/api/calendar-feeds/sync-all/route.tsx +0 -34
  59. package/app/api/cron/calendar-feeds/route.tsx +0 -31
  60. package/app/api/cron/stale-tasks/route.tsx +0 -51
  61. package/app/api/cron/sync/route.tsx +0 -34
  62. package/app/api/devices/[deviceId]/route.tsx +0 -25
  63. package/app/api/devices/route.tsx +0 -41
  64. package/app/api/export/route.tsx +0 -40
  65. package/app/api/feedback/route.tsx +0 -54
  66. package/app/api/folders/[folderId]/route.tsx +0 -51
  67. package/app/api/folders/route.tsx +0 -37
  68. package/app/api/graph/route.tsx +0 -242
  69. package/app/api/guest/route.tsx +0 -58
  70. package/app/api/health/route.tsx +0 -10
  71. package/app/api/holidays/countries/route.tsx +0 -14
  72. package/app/api/holidays/route.tsx +0 -49
  73. package/app/api/holidays/states/route.tsx +0 -21
  74. package/app/api/invites/[token]/route.tsx +0 -131
  75. package/app/api/invites/route.tsx +0 -74
  76. package/app/api/mcp/generate-token/route.tsx +0 -55
  77. package/app/api/mcp/revoke-token/[tokenId]/route.tsx +0 -30
  78. package/app/api/mcp/update-alias/[tokenId]/route.tsx +0 -22
  79. package/app/api/notes/[noteId]/export/route.tsx +0 -45
  80. package/app/api/notes/[noteId]/route.tsx +0 -360
  81. package/app/api/notes/route.tsx +0 -112
  82. package/app/api/notifications/route.tsx +0 -44
  83. package/app/api/register/route.tsx +0 -67
  84. package/app/api/restore/route.tsx +0 -148
  85. package/app/api/sync/conflicts/[conflictId]/route.tsx +0 -134
  86. package/app/api/sync/conflicts/route.tsx +0 -48
  87. package/app/api/sync/status/route.tsx +0 -49
  88. package/app/api/sync/trigger/route.tsx +0 -15
  89. package/app/api/tasks/[taskId]/detail/route.tsx +0 -68
  90. package/app/api/tasks/[taskId]/route.tsx +0 -259
  91. package/app/api/tasks/bulk/route.tsx +0 -133
  92. package/app/api/tasks/route.tsx +0 -36
  93. package/app/api/workspace/active/route.tsx +0 -39
  94. package/app/api/workspace/create-team/route.tsx +0 -42
  95. package/app/api/workspace/kanban-statuses/route.tsx +0 -71
  96. package/app/api/workspace/members/[memberId]/route.tsx +0 -69
  97. package/app/api/workspace/route.tsx +0 -24
  98. package/app/download/page.tsx +0 -170
  99. package/app/favicon.ico +0 -0
  100. package/app/generated/prisma/client.d.ts +0 -1
  101. package/app/generated/prisma/client.js +0 -5
  102. package/app/generated/prisma/default.d.ts +0 -1
  103. package/app/generated/prisma/default.js +0 -5
  104. package/app/generated/prisma/edge.d.ts +0 -1
  105. package/app/generated/prisma/edge.js +0 -497
  106. package/app/generated/prisma/index-browser.js +0 -523
  107. package/app/generated/prisma/index.d.ts +0 -46376
  108. package/app/generated/prisma/index.js +0 -497
  109. package/app/generated/prisma/package.json +0 -144
  110. package/app/generated/prisma/query_compiler_fast_bg.js +0 -2
  111. package/app/generated/prisma/query_compiler_fast_bg.wasm +0 -0
  112. package/app/generated/prisma/query_compiler_fast_bg.wasm-base64.js +0 -2
  113. package/app/generated/prisma/runtime/client.d.ts +0 -3386
  114. package/app/generated/prisma/runtime/client.js +0 -86
  115. package/app/generated/prisma/runtime/index-browser.d.ts +0 -90
  116. package/app/generated/prisma/runtime/index-browser.js +0 -6
  117. package/app/generated/prisma/runtime/wasm-compiler-edge.js +0 -76
  118. package/app/generated/prisma/schema.prisma +0 -456
  119. package/app/generated/prisma/wasm-edge-light-loader.mjs +0 -5
  120. package/app/generated/prisma/wasm-worker-loader.mjs +0 -5
  121. package/app/globals.css +0 -54
  122. package/app/invite/[token]/page.tsx +0 -52
  123. package/app/layout.tsx +0 -90
  124. package/app/mcp/route.tsx +0 -430
  125. package/app/opengraph-image.tsx +0 -120
  126. package/app/page.tsx +0 -398
  127. package/app/privacy/page.tsx +0 -69
  128. package/app/robots.tsx +0 -25
  129. package/app/sitemap.tsx +0 -36
  130. package/app/terms/page.tsx +0 -69
  131. package/app/upgrade/page.tsx +0 -75
  132. package/auth.config.ts +0 -33
  133. package/auth.ts +0 -79
  134. package/components/auth/login-form.tsx +0 -302
  135. package/components/auth/password-checklist.tsx +0 -31
  136. package/components/auth/password-input.tsx +0 -36
  137. package/components/auth/switch-account-button.test.tsx +0 -22
  138. package/components/auth/switch-account-button.tsx +0 -19
  139. package/components/auth/two-factor-input.tsx +0 -116
  140. package/components/billing/billing-dashboard.tsx +0 -265
  141. package/components/billing/card-form.tsx +0 -210
  142. package/components/billing/claim-account-form.tsx +0 -99
  143. package/components/branding/app-logo.test.tsx +0 -20
  144. package/components/branding/app-logo.tsx +0 -25
  145. package/components/calendar/calendar-agenda.tsx +0 -150
  146. package/components/calendar/calendar-drag.test.tsx +0 -177
  147. package/components/calendar/calendar-grid.tsx +0 -357
  148. package/components/calendar/calendar-hooks.test.tsx +0 -27
  149. package/components/calendar/calendar-hooks.ts +0 -351
  150. package/components/calendar/calendar-toolbar.test.tsx +0 -68
  151. package/components/calendar/calendar-toolbar.tsx +0 -291
  152. package/components/calendar/calendar-types.ts +0 -148
  153. package/components/calendar/calendar-view.test.tsx +0 -295
  154. package/components/calendar/calendar-view.tsx +0 -307
  155. package/components/calendar/day-detail-popover.tsx +0 -174
  156. package/components/calendar/task-chip.tsx +0 -86
  157. package/components/command/command-palette.test.tsx +0 -33
  158. package/components/command/command-palette.tsx +0 -310
  159. package/components/download-cta.tsx +0 -87
  160. package/components/feedback/feedback-popup.tsx +0 -207
  161. package/components/graph/graph-draw.ts +0 -337
  162. package/components/graph/graph-overlays.tsx +0 -160
  163. package/components/graph/graph-page.test.tsx +0 -131
  164. package/components/graph/graph-page.tsx +0 -263
  165. package/components/graph/graph-types.ts +0 -47
  166. package/components/graph/graph-view.tsx +0 -322
  167. package/components/guide/guide-view.tsx +0 -522
  168. package/components/kanban/kanban-board.test.tsx +0 -128
  169. package/components/kanban/kanban-board.tsx +0 -361
  170. package/components/kanban/kanban-card-menu.tsx +0 -102
  171. package/components/kanban/kanban-card.tsx +0 -227
  172. package/components/kanban/kanban-column.tsx +0 -49
  173. package/components/kanban/kanban-status-context.tsx +0 -28
  174. package/components/landing/calendar-sandbox.test.tsx +0 -15
  175. package/components/landing/calendar-sandbox.tsx +0 -107
  176. package/components/landing/graph-sandbox.test.tsx +0 -27
  177. package/components/landing/graph-sandbox.tsx +0 -80
  178. package/components/landing/kanban-sandbox.test.tsx +0 -24
  179. package/components/landing/kanban-sandbox.tsx +0 -101
  180. package/components/landing/landing-showcase.test.tsx +0 -21
  181. package/components/landing/landing-showcase.tsx +0 -54
  182. package/components/landing/list-sandbox.tsx +0 -86
  183. package/components/landing/mock-workspace.ts +0 -168
  184. package/components/landing/notes-sandbox.test.tsx +0 -14
  185. package/components/landing/notes-sandbox.tsx +0 -88
  186. package/components/layout/app-shell.tsx +0 -83
  187. package/components/layout/backup-scheduler.tsx +0 -122
  188. package/components/layout/bottom-nav.tsx +0 -43
  189. package/components/layout/icon-bar.test.tsx +0 -29
  190. package/components/layout/icon-bar.tsx +0 -118
  191. package/components/layout/mobile-top-bar.tsx +0 -68
  192. package/components/layout/notes-panel-folder.tsx +0 -127
  193. package/components/layout/notes-panel-note-item.tsx +0 -140
  194. package/components/layout/notes-panel-task-tab.tsx +0 -63
  195. package/components/layout/notes-panel-types.ts +0 -44
  196. package/components/layout/notes-panel.tsx +0 -476
  197. package/components/layout/notification-bell.tsx +0 -251
  198. package/components/layout/paywall-screen.tsx +0 -41
  199. package/components/layout/pro-banner.tsx +0 -76
  200. package/components/layout/sw-register.tsx +0 -27
  201. package/components/layout/workspace-switcher.tsx +0 -90
  202. package/components/notes/mobile-bottom-sheet.tsx +0 -99
  203. package/components/notes/note-editor-context-menu.tsx +0 -47
  204. package/components/notes/note-editor-dom.ts +0 -33
  205. package/components/notes/note-editor-dropdowns.tsx +0 -484
  206. package/components/notes/note-editor-hooks.ts +0 -692
  207. package/components/notes/note-editor-keyboard.ts +0 -305
  208. package/components/notes/note-editor-overlay.tsx +0 -90
  209. package/components/notes/note-editor.test.tsx +0 -372
  210. package/components/notes/note-editor.tsx +0 -662
  211. package/components/notes/note-preview-pane.tsx +0 -156
  212. package/components/notes/note-tabs.tsx +0 -120
  213. package/components/notes/note-types.tsx +0 -157
  214. package/components/settings/accept-invite.tsx +0 -108
  215. package/components/settings/agent-token-settings.tsx +0 -369
  216. package/components/settings/backup-restore-settings.test.tsx +0 -25
  217. package/components/settings/backup-restore-settings.tsx +0 -327
  218. package/components/settings/calendar-feeds-settings.tsx +0 -489
  219. package/components/settings/calendar-general-settings.tsx +0 -174
  220. package/components/settings/confirm-danger-action.test.tsx +0 -215
  221. package/components/settings/confirm-danger-action.tsx +0 -65
  222. package/components/settings/security-settings.tsx +0 -252
  223. package/components/settings/settings-guidance.test.tsx +0 -98
  224. package/components/settings/team-settings.tsx +0 -319
  225. package/components/settings/two-factor-auth.tsx +0 -296
  226. package/components/settings/workspace-settings-client.tsx +0 -363
  227. package/components/settings/workspace-settings-form.tsx +0 -73
  228. package/components/sync/conflict-viewer.tsx +0 -247
  229. package/components/sync/sync-indicator.tsx +0 -171
  230. package/components/tasks/snippet-thread.tsx +0 -119
  231. package/components/tasks/status-dot.tsx +0 -47
  232. package/components/tasks/task-badge.tsx +0 -43
  233. package/components/tasks/task-detail.test.tsx +0 -187
  234. package/components/tasks/task-detail.tsx +0 -458
  235. package/components/tasks/task-list-filters.test.tsx +0 -75
  236. package/components/tasks/task-list-filters.tsx +0 -163
  237. package/components/tasks/task-list-types.ts +0 -20
  238. package/components/tasks/task-list.test.tsx +0 -175
  239. package/components/tasks/task-list.tsx +0 -481
  240. package/components/tasks/task-row.tsx +0 -85
  241. package/components/tasks/task-table-row.tsx +0 -259
  242. package/components/ui/skeleton.tsx +0 -3
  243. package/components/ui/toast.test.tsx +0 -42
  244. package/components/ui/toast.tsx +0 -70
  245. package/electron/main.ts +0 -251
  246. package/electron/preload.ts +0 -56
  247. package/instrumentation.tsx +0 -23
  248. package/lib/api-error.ts +0 -50
  249. package/lib/backup/backup-runner.test.ts +0 -32
  250. package/lib/backup/backup-runner.ts +0 -19
  251. package/lib/backup/backup-schedule.test.ts +0 -23
  252. package/lib/backup/backup-schedule.ts +0 -55
  253. package/lib/backup/backup-settings.test.ts +0 -30
  254. package/lib/backup/backup-settings.ts +0 -27
  255. package/lib/backup/export-notes-zip.test.ts +0 -26
  256. package/lib/backup/export-notes-zip.ts +0 -82
  257. package/lib/backup/export-workspace-backup.test.ts +0 -17
  258. package/lib/backup/export-workspace-backup.ts +0 -77
  259. package/lib/backup/restore-workspace-from-export.test.ts +0 -18
  260. package/lib/backup/restore-workspace-from-export.ts +0 -183
  261. package/lib/backup/types.ts +0 -14
  262. package/lib/brand-icons.ts +0 -1
  263. package/lib/calendar-feed-crypto.ts +0 -38
  264. package/lib/calendar-feed.ts +0 -239
  265. package/lib/client/online-status.ts +0 -47
  266. package/lib/conflict-resolver.test.ts +0 -57
  267. package/lib/conflict-resolver.ts +0 -240
  268. package/lib/db-init.ts +0 -79
  269. package/lib/email.ts +0 -159
  270. package/lib/encryption.test.ts +0 -41
  271. package/lib/encryption.ts +0 -98
  272. package/lib/extract-snippet.test.ts +0 -123
  273. package/lib/extract-snippet.ts +0 -69
  274. package/lib/kanban-status.ts +0 -55
  275. package/lib/license.ts +0 -21
  276. package/lib/limits.ts +0 -31
  277. package/lib/mcp-auth.test.ts +0 -58
  278. package/lib/mcp-auth.ts +0 -65
  279. package/lib/mcp-contract.test.ts +0 -25
  280. package/lib/mcp-contract.ts +0 -210
  281. package/lib/mcp-handler.ts +0 -31
  282. package/lib/mcp-url.test.ts +0 -12
  283. package/lib/mcp-url.ts +0 -7
  284. package/lib/mentions.test.ts +0 -45
  285. package/lib/mentions.ts +0 -73
  286. package/lib/note-crypto.ts +0 -108
  287. package/lib/note-sync.ts +0 -201
  288. package/lib/note-title.ts +0 -93
  289. package/lib/prisma.ts +0 -193
  290. package/lib/pro-flush.ts +0 -292
  291. package/lib/rate-limit.ts +0 -57
  292. package/lib/stripe.ts +0 -38
  293. package/lib/sync-worker.ts +0 -388
  294. package/lib/task-parser.test.ts +0 -91
  295. package/lib/task-parser.ts +0 -81
  296. package/lib/task-utils.ts +0 -52
  297. package/lib/use-is-electron.ts +0 -19
  298. package/lib/use-is-mobile.ts +0 -22
  299. package/lib/validation/calendar-feed.ts +0 -31
  300. package/lib/validation/note.ts +0 -27
  301. package/lib/validation/task.ts +0 -26
  302. package/lib/view-preferences.test.ts +0 -54
  303. package/lib/view-preferences.ts +0 -28
  304. package/lib/workspace.ts +0 -66
  305. package/next.config.ts +0 -21
  306. package/postcss.config.mjs +0 -7
  307. package/prisma/migrations/20260519021916_init/migration.sql +0 -388
  308. package/prisma/migrations/20260519061113_drop_sync_password/migration.sql +0 -8
  309. package/prisma/migrations/20260520065016_add_task_start_date/migration.sql +0 -2
  310. package/prisma/migrations/20260529010600_remove_encryption_fields/migration.sql +0 -12
  311. package/prisma/migrations/20260529020000_restore_encryption_salt/migration.sql +0 -3
  312. package/prisma/migrations/20260529030000_add_folders/migration.sql +0 -17
  313. package/prisma/migrations/20260605000000_deferred_fixes/migration.sql +0 -31
  314. package/prisma/migrations/20260605020806_add_pending_sync_to_note_and_task/migration.sql +0 -5
  315. package/prisma/migrations/20260605063634_add_stripe_webhook_event_sync_lock/migration.sql +0 -14
  316. package/prisma/migrations/20260605100000_add_prod_indexes/migration.sql +0 -26
  317. package/prisma/migrations/20260608081404_add_kanban_statuses/migration.sql +0 -23
  318. package/prisma/migrations/20260611032723_add_calendar_feeds/migration.sql +0 -43
  319. package/prisma/migrations/20260611040000_add_calendar_feed_color/migration.sql +0 -2
  320. package/prisma/migrations/20260611050000_add_task_priority/migration.sql +0 -14
  321. package/prisma/migrations/20260612060000_add_critical_priority/migration.sql +0 -2
  322. package/prisma/migrations/20260613090000_add_backup_settings/migration.sql +0 -25
  323. package/prisma/migrations/20260614160000_add_feedback/migration.sql +0 -20
  324. package/prisma/migrations/20260614210000_add_2fa/migration.sql +0 -4
  325. package/prisma/migrations/migration_lock.toml +0 -3
  326. package/prisma/schema.prisma +0 -457
  327. package/public/Logo_icon.svg +0 -1
  328. package/public/file.svg +0 -1
  329. package/public/globe.svg +0 -1
  330. package/public/icon-192.png +0 -0
  331. package/public/icon-512.png +0 -0
  332. package/public/icon.svg +0 -4
  333. package/public/icon_dark.svg +0 -1
  334. package/public/knotpad_icon.svg +0 -1
  335. package/public/knotpad_logo_full.svg +0 -1
  336. package/public/manifest.json +0 -14
  337. package/public/next.svg +0 -1
  338. package/public/sw.js +0 -137
  339. package/public/vercel.svg +0 -1
  340. package/public/window.svg +0 -1
  341. package/tsconfig.json +0 -35
@@ -1,458 +0,0 @@
1
- "use client";
2
-
3
- import { useState, useEffect } from "react";
4
- import { useRouter } from "next/navigation";
5
- import { X, FileText, Clock } from "lucide-react";
6
- import Link from "next/link";
7
- import { TaskBadge } from "./task-badge";
8
- import { useKanbanStatuses } from "@/components/kanban/kanban-status-context";
9
- import { SnippetThread } from "./snippet-thread";
10
-
11
- type AuditEntry = {
12
- id: string;
13
- action: string;
14
- detail: string | null;
15
- createdAt: string;
16
- user: { id: string; name: string | null } | null;
17
- };
18
-
19
- type Reference = { noteId: string; noteTitle: string; snippet: string };
20
-
21
- type Task = {
22
- id: string;
23
- title: string;
24
- status: string;
25
- priority?: string | null;
26
- assigneeType: "HUMAN" | "AGENT";
27
- claimedBy: string | null;
28
- claimedByAlias: string | null;
29
- fileRefs: string[];
30
- startDate: string | null;
31
- dueDate: string | null;
32
- note: { id: string; title: string };
33
- assignee: { id: string; name: string | null; email: string | null; image: string | null } | null;
34
- auditLogs: AuditEntry[];
35
- references: Reference[];
36
- };
37
-
38
- type Props = { task: Task; backHref: string };
39
-
40
- // Format relative time (e.g., "3d ago", "2h ago", "5m ago")
41
- function formatRelativeTime(date: string | Date): string {
42
- const now = new Date();
43
- const then = new Date(date);
44
- const diffMs = now.getTime() - then.getTime();
45
- const diffSec = Math.floor(diffMs / 1000);
46
- const diffMin = Math.floor(diffSec / 60);
47
- const diffHour = Math.floor(diffMin / 60);
48
- const diffDay = Math.floor(diffHour / 24);
49
- const diffWeek = Math.floor(diffDay / 7);
50
- const diffMonth = Math.floor(diffDay / 30);
51
- const diffYear = Math.floor(diffDay / 365);
52
-
53
- if (diffSec < 60) return "just now";
54
- if (diffMin < 60) return `${diffMin}m ago`;
55
- if (diffHour < 24) return `${diffHour}h ago`;
56
- if (diffDay < 7) return `${diffDay}d ago`;
57
- if (diffWeek < 4) return `${diffWeek}w ago`;
58
- if (diffMonth < 12) return `${diffMonth}mo ago`;
59
- return `${diffYear}y ago`;
60
- }
61
-
62
- // Format date for date input (YYYY-MM-DD) in local timezone
63
- function formatDateForInput(date: string | Date | null): string {
64
- if (!date) return "";
65
- const d = new Date(date);
66
- const year = d.getFullYear();
67
- const month = String(d.getMonth() + 1).padStart(2, "0");
68
- const day = String(d.getDate()).padStart(2, "0");
69
- return `${year}-${month}-${day}`;
70
- }
71
-
72
- function getBackLabel(backHref: string): string {
73
- if (backHref.startsWith("/list")) return "Back to List";
74
- if (backHref.startsWith("/kanban")) return "Back to Kanban";
75
- if (backHref.startsWith("/calendar")) return "Back to Calendar";
76
- if (backHref.startsWith("/notes")) return "Back to Note";
77
- return "Back";
78
- }
79
-
80
- export function TaskDetail({ task, backHref }: Props) {
81
- const router = useRouter();
82
- const kanbanStatuses = useKanbanStatuses();
83
- const statusOptions = kanbanStatuses.map((s) => s.key);
84
- const backLabel = getBackLabel(backHref);
85
- const [status, setStatus] = useState<string>(task.status);
86
- const [updating, setUpdating] = useState(false);
87
- const [error, setError] = useState("");
88
- const [successMessage, setSuccessMessage] = useState("");
89
- const [startDate, setStartDate] = useState<string>(formatDateForInput(task.startDate));
90
- const [savingStart, setSavingStart] = useState(false);
91
- const [dueDate, setDueDate] = useState<string>(formatDateForInput(task.dueDate));
92
- const [savingDue, setSavingDue] = useState(false);
93
- const [showAllActivity, setShowAllActivity] = useState(false);
94
- const [priority, setPriority] = useState<string>(task.priority ?? "medium");
95
- const [savingPriority, setSavingPriority] = useState(false);
96
-
97
- // Sync local state when task prop changes (e.g., after router.refresh())
98
- useEffect(() => {
99
- setStatus(task.status);
100
- setPriority(task.priority ?? "medium");
101
- setStartDate(formatDateForInput(task.startDate));
102
- setDueDate(formatDateForInput(task.dueDate));
103
- }, [task.dueDate, task.priority, task.startDate, task.status]);
104
-
105
- async function handleStatusChange(newStatus: string) {
106
- if (newStatus === status) return;
107
- const prev = status;
108
- setUpdating(true);
109
- setStatus(newStatus);
110
- setError("");
111
- try {
112
- const res = await fetch(`/api/tasks/${task.id}`, {
113
- method: "PATCH",
114
- headers: { "Content-Type": "application/json" },
115
- body: JSON.stringify({ status: newStatus }),
116
- });
117
- if (!res.ok) throw new Error("patch failed");
118
- router.refresh();
119
- } catch {
120
- setStatus(prev);
121
- setError("Failed to update status. Please try again.");
122
- } finally {
123
- setUpdating(false);
124
- }
125
- }
126
-
127
- async function handleStartDateChange(val: string) {
128
- const prev = startDate;
129
- setStartDate(val);
130
- setSavingStart(true);
131
- setError("");
132
- try {
133
- const res = await fetch(`/api/tasks/${task.id}`, {
134
- method: "PATCH",
135
- headers: { "Content-Type": "application/json" },
136
- body: JSON.stringify({ startDate: val || null }),
137
- });
138
- if (!res.ok) throw new Error();
139
- router.refresh();
140
- } catch {
141
- setStartDate(prev);
142
- setError("Failed to update start date.");
143
- } finally {
144
- setSavingStart(false);
145
- }
146
- }
147
-
148
- async function handleDueDateChange(val: string) {
149
- const prev = dueDate;
150
- setDueDate(val);
151
- setSavingDue(true);
152
- setError("");
153
- setSuccessMessage("");
154
- try {
155
- const res = await fetch(`/api/tasks/${task.id}`, {
156
- method: "PATCH",
157
- headers: { "Content-Type": "application/json" },
158
- body: JSON.stringify({ dueDate: val || null }),
159
- });
160
- if (!res.ok) throw new Error();
161
- setSuccessMessage(val ? "Due date updated." : "Due date cleared.");
162
- router.refresh();
163
- } catch {
164
- setDueDate(prev);
165
- setError("Failed to update due date.");
166
- } finally {
167
- setSavingDue(false);
168
- }
169
- }
170
-
171
- async function handlePriorityChange(newPriority: string) {
172
- const normalizedPriority = newPriority.toLowerCase();
173
- if (normalizedPriority === priority) return;
174
- const prev = priority;
175
- setPriority(normalizedPriority);
176
- setSavingPriority(true);
177
- setError("");
178
- setSuccessMessage("");
179
- try {
180
- const res = await fetch(`/api/tasks/${task.id}`, {
181
- method: "PATCH",
182
- headers: { "Content-Type": "application/json" },
183
- body: JSON.stringify({ priority: normalizedPriority.toUpperCase() }),
184
- });
185
- if (!res.ok) throw new Error();
186
- setSuccessMessage("Priority updated.");
187
- router.refresh();
188
- } catch {
189
- setPriority(prev);
190
- setError("Failed to update priority.");
191
- } finally {
192
- setSavingPriority(false);
193
- }
194
- }
195
-
196
- // Task 2.5.4 - Assignee picker
197
- const [assigneeType, setAssigneeType] = useState<"HUMAN" | "AGENT">(task.assigneeType);
198
- const [savingAssignee, setSavingAssignee] = useState(false);
199
-
200
- async function handleAssigneeChange(type: "HUMAN" | "AGENT") {
201
- const prev = assigneeType;
202
- setAssigneeType(type);
203
- setSavingAssignee(true);
204
- setError("");
205
- try {
206
- const res = await fetch(`/api/tasks/${task.id}`, {
207
- method: "PATCH",
208
- headers: { "Content-Type": "application/json" },
209
- body: JSON.stringify({ assigneeType: type }),
210
- });
211
- if (!res.ok) throw new Error();
212
- router.refresh();
213
- } catch {
214
- setAssigneeType(prev);
215
- setError("Failed to update assignee.");
216
- } finally {
217
- setSavingAssignee(false);
218
- }
219
- }
220
-
221
- const displayLogs = showAllActivity ? task.auditLogs : task.auditLogs.slice(0, 5);
222
- const hasMoreLogs = task.auditLogs.length > 5;
223
- const summaryChips = [
224
- dueDate && new Date(dueDate) < new Date() && status !== "DONE" ? "Overdue" : null,
225
- assigneeType === "AGENT" ? "Agent task" : "Human task",
226
- `References ${task.references.length}`,
227
- task.auditLogs[0]?.createdAt ? `Updated ${formatRelativeTime(task.auditLogs[0].createdAt)}` : null,
228
- ].filter((chip): chip is string => Boolean(chip));
229
-
230
- const auditSection = task.auditLogs.length > 0 && (
231
- <div className="space-y-2">
232
- <div className="flex items-center justify-between">
233
- <label className="text-xs font-medium text-zinc-500">Activity</label>
234
- {hasMoreLogs && (
235
- <button
236
- onClick={() => setShowAllActivity(!showAllActivity)}
237
- className="text-xs text-zinc-400 hover:text-zinc-300 transition-colors"
238
- >
239
- {showAllActivity ? "Show less" : `Show ${task.auditLogs.length - 5} more`}
240
- </button>
241
- )}
242
- </div>
243
- <div className="space-y-2">
244
- {displayLogs.map((log) => (
245
- <div key={log.id} className="flex items-start gap-2 text-xs">
246
- <Clock size={11} className="mt-0.5 shrink-0 text-zinc-600" />
247
- <div className="min-w-0 space-y-1">
248
- <div className="flex flex-wrap items-center gap-1">
249
- <span className="font-medium text-zinc-300">{log.user?.name ?? "System"}</span>
250
- <span className="text-zinc-500">{log.action.replace(/_/g, " ")}</span>
251
- </div>
252
- {log.detail && (
253
- <p className="text-zinc-600 truncate">{log.detail}</p>
254
- )}
255
- <p className="text-zinc-700" title={new Date(log.createdAt).toLocaleString()}>
256
- {formatRelativeTime(log.createdAt)}
257
- </p>
258
- </div>
259
- </div>
260
- ))}
261
- </div>
262
- </div>
263
- );
264
-
265
- return (
266
- <div className="flex h-full flex-col overflow-hidden">
267
- {/* Header */}
268
- <div className="flex items-center justify-between border-b border-zinc-800 px-4 py-3 md:px-6">
269
- <Link
270
- href={backHref}
271
- className="flex items-center gap-1.5 text-xs text-zinc-500 hover:text-zinc-300 transition-colors"
272
- >
273
- <X size={14} />
274
- <span>{backLabel}</span>
275
- </Link>
276
- <nav className="min-w-0 text-xs text-zinc-600">
277
- <Link href={`/notes/${task.note.id}`} className="block truncate hover:text-zinc-400 transition-colors">
278
- {task.note.title}
279
- </Link>
280
- </nav>
281
- </div>
282
-
283
- {error && (
284
- <div className="border-b border-red-800/40 bg-red-950/40 px-4 py-2 flex items-center justify-between gap-3 md:px-6">
285
- <p className="text-sm text-red-300">{error}</p>
286
- <button onClick={() => setError("")} className="text-red-500 hover:text-red-300 text-lg leading-none shrink-0">×</button>
287
- </div>
288
- )}
289
-
290
- {successMessage && !error && (
291
- <div className="border-b border-emerald-800/40 bg-emerald-950/40 px-4 py-2 md:px-6">
292
- <p className="text-sm text-emerald-300">{successMessage}</p>
293
- </div>
294
- )}
295
-
296
- {summaryChips.length > 0 && (
297
- <div className="flex flex-wrap gap-2 border-b border-zinc-800 px-4 py-2 md:px-6">
298
- {summaryChips.map((chip) => (
299
- <span
300
- key={chip}
301
- className="rounded-full border border-zinc-800 bg-zinc-900 px-2.5 py-1 text-xs text-zinc-300"
302
- >
303
- {chip}
304
- </span>
305
- ))}
306
- </div>
307
- )}
308
-
309
- {/* Body */}
310
- <div className="flex flex-1 flex-col overflow-y-auto md:flex-row md:overflow-hidden md:divide-x md:divide-zinc-800">
311
- {/* Left — task details */}
312
- <div className="flex w-full shrink-0 flex-col gap-6 border-b border-zinc-800 p-4 md:w-[380px] md:overflow-y-auto md:border-b-0 md:p-6">
313
- <h1 className="text-xl font-semibold text-zinc-100 leading-snug">{task.title}</h1>
314
-
315
- {/* Status */}
316
- <div className="space-y-1.5">
317
- <label className="text-xs font-medium text-zinc-500">Status</label>
318
- <div className="flex flex-wrap gap-1.5">
319
- {statusOptions.map((s) => (
320
- <button
321
- key={s}
322
- disabled={updating}
323
- onClick={() => handleStatusChange(s)}
324
- className={`transition-opacity disabled:opacity-50 ${
325
- status === s ? "ring-1 ring-white/20 rounded" : "opacity-60 hover:opacity-90"
326
- }`}
327
- >
328
- <TaskBadge status={s} />
329
- </button>
330
- ))}
331
- </div>
332
- </div>
333
-
334
- {/* Assignee - Task 2.5.4 */}
335
- <div className="space-y-1.5">
336
- <label className="text-xs font-medium text-zinc-500">Assignee</label>
337
- <select
338
- value={assigneeType}
339
- onChange={(e) => handleAssigneeChange(e.target.value as "HUMAN" | "AGENT")}
340
- disabled={savingAssignee}
341
- className={`text-sm bg-zinc-800 border border-zinc-700 rounded px-2 py-1 w-full cursor-pointer ${
342
- savingAssignee ? "opacity-50" : ""
343
- } ${task.assigneeType === "AGENT" ? "text-blue-400" : "text-zinc-300"}`}
344
- >
345
- <option value="HUMAN">Human</option>
346
- <option value="AGENT">Agent</option>
347
- </select>
348
- {savingAssignee && <span className="text-xs text-zinc-500">Saving...</span>}
349
- </div>
350
-
351
- <div className="space-y-1.5">
352
- <label className="text-xs font-medium text-zinc-500">
353
- Priority {savingPriority && <span className="font-normal text-zinc-600">saving...</span>}
354
- </label>
355
- <select
356
- value={priority}
357
- onChange={(e) => handlePriorityChange(e.target.value)}
358
- disabled={savingPriority}
359
- className={`w-full rounded border border-zinc-700 bg-zinc-800 px-2 py-1 text-sm text-zinc-300 ${
360
- savingPriority ? "opacity-50" : ""
361
- }`}
362
- >
363
- <option value="low">low</option>
364
- <option value="medium">medium</option>
365
- <option value="high">high</option>
366
- <option value="critical">critical</option>
367
- </select>
368
- </div>
369
-
370
- {/* Source note */}
371
- <div className="space-y-1.5">
372
- <label className="text-xs font-medium text-zinc-500">Continue in source note</label>
373
- <Link
374
- href={`/notes/${task.note.id}`}
375
- className="flex items-center gap-1.5 text-sm text-zinc-300 hover:text-white transition-colors"
376
- >
377
- <FileText size={13} />
378
- {task.note.title}
379
- </Link>
380
- </div>
381
-
382
- {/* File refs */}
383
- {task.fileRefs.length > 0 && (
384
- <div className="space-y-1.5">
385
- <label className="text-xs font-medium text-zinc-500">File references</label>
386
- <div className="flex flex-wrap gap-1.5">
387
- {task.fileRefs.map((f) => (
388
- <span
389
- key={f}
390
- className="rounded border border-zinc-700 bg-zinc-800 px-2 py-0.5 font-mono text-xs text-emerald-400"
391
- >
392
- {f}
393
- </span>
394
- ))}
395
- </div>
396
- </div>
397
- )}
398
-
399
- {/* Start date */}
400
- <div className="space-y-1.5">
401
- <label className="text-xs font-medium text-zinc-500">
402
- Start date {savingStart && <span className="text-zinc-600 font-normal">saving…</span>}
403
- </label>
404
- <input
405
- type="date"
406
- value={startDate}
407
- onChange={(e) => handleStartDateChange(e.target.value)}
408
- className="rounded-md border border-zinc-700 bg-zinc-900 px-2 py-1 text-base text-zinc-300 focus:border-zinc-500 focus:outline-none ring-focus [color-scheme:dark]"
409
- />
410
- {startDate && (
411
- <button
412
- onClick={() => handleStartDateChange("")}
413
- className="block text-xs text-zinc-600 hover:text-zinc-400 transition-colors"
414
- >
415
- Clear date
416
- </button>
417
- )}
418
- </div>
419
-
420
- {/* Due date */}
421
- <div className="space-y-1.5">
422
- <label className="text-xs font-medium text-zinc-500">
423
- Due date {savingDue && <span className="text-zinc-600 font-normal">saving…</span>}
424
- </label>
425
- <input
426
- type="date"
427
- value={dueDate}
428
- onChange={(e) => handleDueDateChange(e.target.value)}
429
- className="rounded-md border border-zinc-700 bg-zinc-900 px-2 py-1 text-base text-zinc-300 focus:border-zinc-500 focus:outline-none ring-focus [color-scheme:dark]"
430
- />
431
- {dueDate && (
432
- <button
433
- onClick={() => handleDueDateChange("")}
434
- className="block text-xs text-zinc-600 hover:text-zinc-400 transition-colors"
435
- >
436
- Clear date
437
- </button>
438
- )}
439
- </div>
440
-
441
- {/* Audit trail — desktop only here; on mobile it moves below the
442
- note references (see bottom of this layout). */}
443
- {auditSection && <div className="hidden md:block">{auditSection}</div>}
444
- </div>
445
-
446
- {/* Right — snippet thread */}
447
- <div className="flex-1 p-4 md:overflow-y-auto md:p-6">
448
- <SnippetThread references={task.references} taskTitle={task.title} />
449
- </div>
450
-
451
- {/* Audit trail — mobile only, after note references */}
452
- {auditSection && (
453
- <div className="border-t border-zinc-800 p-4 md:hidden">{auditSection}</div>
454
- )}
455
- </div>
456
- </div>
457
- );
458
- }
@@ -1,75 +0,0 @@
1
- // @vitest-environment jsdom
2
- import { cleanup, render, screen } from "@testing-library/react";
3
- import userEvent from "@testing-library/user-event";
4
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
- import { TaskListFilters } from "@/components/tasks/task-list-filters";
6
-
7
- const pushMock = vi.fn();
8
- let pathname = "/list";
9
- let searchParams = new URLSearchParams();
10
-
11
- vi.mock("next/navigation", () => ({
12
- useRouter: () => ({ push: pushMock, refresh: vi.fn() }),
13
- usePathname: () => pathname,
14
- useSearchParams: () => searchParams,
15
- }));
16
-
17
- beforeEach(() => {
18
- pushMock.mockReset();
19
- pathname = "/list";
20
- searchParams = new URLSearchParams();
21
- });
22
-
23
- afterEach(() => {
24
- cleanup();
25
- });
26
-
27
- describe("TaskListFilters", () => {
28
- it("shows active filter chips and removes only the clicked chip", async () => {
29
- const user = userEvent.setup();
30
- searchParams = new URLSearchParams(
31
- "note=n1&folder=f1&status=DONE&assigneeType=AGENT&overdue=true&hasDueDate=true"
32
- );
33
-
34
- render(
35
- <TaskListFilters
36
- folders={[{ id: "f1", name: "Product" }]}
37
- filterFolderId="f1"
38
- statusFilter="DONE"
39
- assigneeFilter="AGENT"
40
- overdueOnly="true"
41
- hasDueDateFilter="true"
42
- />
43
- );
44
-
45
- expect(screen.getByRole("button", { name: "Folder: Product ×" })).toBeInTheDocument();
46
- expect(screen.getByRole("button", { name: "Status: DONE ×" })).toBeInTheDocument();
47
- expect(screen.getByRole("button", { name: "Assignee: AGENT ×" })).toBeInTheDocument();
48
- expect(screen.getByRole("button", { name: "Overdue ×" })).toBeInTheDocument();
49
- expect(screen.getByRole("button", { name: "Has due date ×" })).toBeInTheDocument();
50
-
51
- await user.click(screen.getByRole("button", { name: "Status: DONE ×" }));
52
-
53
- expect(pushMock).toHaveBeenCalledWith(
54
- "/list?note=n1&folder=f1&assigneeType=AGENT&overdue=true&hasDueDate=true"
55
- );
56
- });
57
-
58
- it("clears only filter params and preserves unrelated query state", async () => {
59
- const user = userEvent.setup();
60
- searchParams = new URLSearchParams("note=n1&folder=f1&status=DONE&overdue=true");
61
-
62
- render(
63
- <TaskListFilters
64
- folders={[{ id: "f1", name: "Product" }]}
65
- filterFolderId="f1"
66
- statusFilter="DONE"
67
- overdueOnly="true"
68
- />
69
- );
70
-
71
- await user.click(screen.getByRole("button", { name: "Clear all" }));
72
-
73
- expect(pushMock).toHaveBeenCalledWith("/list?note=n1");
74
- });
75
- });
@@ -1,163 +0,0 @@
1
- "use client";
2
-
3
- import { usePathname, useRouter, useSearchParams } from "next/navigation";
4
-
5
- type Props = {
6
- folders: { id: string; name: string }[];
7
- filterFolderId?: string;
8
- statusFilter?: string;
9
- assigneeFilter?: string;
10
- overdueOnly?: string;
11
- hasDueDateFilter?: string;
12
- };
13
-
14
- export function TaskListFilters({
15
- folders,
16
- filterFolderId,
17
- statusFilter,
18
- assigneeFilter,
19
- overdueOnly,
20
- hasDueDateFilter,
21
- }: Props) {
22
- const router = useRouter();
23
- const pathname = usePathname();
24
- const searchParams = useSearchParams();
25
-
26
- function pushParams(params: URLSearchParams) {
27
- const query = params.toString();
28
- router.push(query ? `${pathname}?${query}` : pathname);
29
- }
30
-
31
- function setParam(key: string, value: string | null) {
32
- const params = new URLSearchParams(searchParams.toString());
33
- if (value) params.set(key, value);
34
- else params.delete(key);
35
- pushParams(params);
36
- }
37
-
38
- function toggleParam(key: string) {
39
- const params = new URLSearchParams(searchParams.toString());
40
- if (params.get(key) === "true") params.delete(key);
41
- else params.set(key, "true");
42
- pushParams(params);
43
- }
44
-
45
- function clearFilters() {
46
- const params = new URLSearchParams(searchParams.toString());
47
- for (const key of ["folder", "status", "assigneeType", "overdue", "hasDueDate"]) {
48
- params.delete(key);
49
- }
50
- pushParams(params);
51
- }
52
-
53
- const hasActiveFilters = Boolean(
54
- statusFilter || assigneeFilter || overdueOnly || hasDueDateFilter || filterFolderId
55
- );
56
- const activeFilters: { key: string; label: string }[] = [];
57
-
58
- if (filterFolderId) {
59
- const folderName = folders.find((folder) => folder.id === filterFolderId)?.name ?? filterFolderId;
60
- activeFilters.push({ key: "folder", label: `Folder: ${folderName}` });
61
- }
62
- if (statusFilter) {
63
- activeFilters.push({ key: "status", label: `Status: ${statusFilter}` });
64
- }
65
- if (assigneeFilter) {
66
- activeFilters.push({ key: "assigneeType", label: `Assignee: ${assigneeFilter}` });
67
- }
68
- if (overdueOnly === "true") {
69
- activeFilters.push({ key: "overdue", label: "Overdue" });
70
- }
71
- if (hasDueDateFilter === "true") {
72
- activeFilters.push({ key: "hasDueDate", label: "Has due date" });
73
- }
74
-
75
- return (
76
- <div className="flex flex-wrap items-center gap-2 px-3 md:px-6 py-2 border-b border-zinc-800/50 bg-zinc-900/30">
77
- {/* Folder Filter */}
78
- {folders.length > 0 && (
79
- <select
80
- value={filterFolderId || ""}
81
- onChange={(e) => setParam("folder", e.target.value || null)}
82
- className="text-xs bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-300"
83
- >
84
- <option value="">All folders</option>
85
- {folders.map((f) => (
86
- <option key={f.id} value={f.id}>{f.name}</option>
87
- ))}
88
- </select>
89
- )}
90
-
91
- {/* Status Filter */}
92
- <select
93
- value={statusFilter || ""}
94
- onChange={(e) => setParam("status", e.target.value || null)}
95
- className="text-xs bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-300"
96
- >
97
- <option value="">All statuses</option>
98
- <option value="TODO,DOING">Active (To Do / Doing)</option>
99
- <option value="DONE">Done</option>
100
- <option value="ARCHIVED">Archived</option>
101
- </select>
102
-
103
- {/* Assignee Filter */}
104
- <select
105
- value={assigneeFilter || ""}
106
- onChange={(e) => setParam("assigneeType", e.target.value || null)}
107
- className="text-xs bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-300"
108
- >
109
- <option value="">All assignees</option>
110
- <option value="HUMAN">Human</option>
111
- <option value="AGENT">Agent</option>
112
- </select>
113
-
114
- {/* Overdue Toggle */}
115
- <button
116
- onClick={() => toggleParam("overdue")}
117
- className={`text-xs px-2 py-1 rounded border transition-colors ${
118
- overdueOnly === "true"
119
- ? "bg-red-900/30 border-red-700 text-red-300"
120
- : "bg-zinc-800 border-zinc-700 text-zinc-400 hover:text-zinc-300"
121
- }`}
122
- >
123
- Overdue only
124
- </button>
125
-
126
- {/* Has Due Date Toggle */}
127
- <button
128
- onClick={() => toggleParam("hasDueDate")}
129
- className={`text-xs px-2 py-1 rounded border transition-colors ${
130
- hasDueDateFilter === "true"
131
- ? "bg-blue-900/30 border-blue-700 text-blue-300"
132
- : "bg-zinc-800 border-zinc-700 text-zinc-400 hover:text-zinc-300"
133
- }`}
134
- >
135
- Has due date
136
- </button>
137
-
138
- {activeFilters.length > 0 && (
139
- <div className="flex flex-wrap items-center gap-2">
140
- {activeFilters.map((filter) => (
141
- <button
142
- key={filter.key}
143
- onClick={() => setParam(filter.key, null)}
144
- className="rounded-full border border-zinc-700 bg-zinc-800 px-2 py-1 text-xs text-zinc-300"
145
- >
146
- {filter.label} ×
147
- </button>
148
- ))}
149
- </div>
150
- )}
151
-
152
- {/* Clear Filters */}
153
- {hasActiveFilters && (
154
- <button
155
- onClick={clearFilters}
156
- className="text-xs text-zinc-500 hover:text-zinc-300 ml-2"
157
- >
158
- Clear all
159
- </button>
160
- )}
161
- </div>
162
- );
163
- }