@knotpad/app 0.1.5 → 0.1.7
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/app/(app)/calendar/page.tsx +57 -0
- package/app/(app)/error.tsx +35 -0
- package/app/(app)/graph/page.tsx +32 -0
- package/app/(app)/guide/page.tsx +21 -0
- package/app/(app)/kanban/loading.tsx +24 -0
- package/app/(app)/kanban/page.tsx +59 -0
- package/app/(app)/layout.tsx +122 -0
- package/app/(app)/list/loading.tsx +21 -0
- package/app/(app)/list/page.tsx +137 -0
- package/app/(app)/loading.tsx +18 -0
- package/app/(app)/notes/[noteId]/page.tsx +84 -0
- package/app/(app)/notes/layout.tsx +30 -0
- package/app/(app)/notes/page.tsx +39 -0
- package/app/(app)/page.tsx +5 -0
- package/app/(app)/settings/agent-token/page.tsx +59 -0
- package/app/(app)/settings/backup/page.tsx +49 -0
- package/app/(app)/settings/billing/page.tsx +53 -0
- package/app/(app)/settings/calendar/page.tsx +41 -0
- package/app/(app)/settings/layout.test.tsx +39 -0
- package/app/(app)/settings/layout.tsx +71 -0
- package/app/(app)/settings/page.tsx +4 -0
- package/app/(app)/settings/security/page.tsx +43 -0
- package/app/(app)/settings/team/page.tsx +74 -0
- package/app/(app)/settings/workspace/page.tsx +27 -0
- package/app/(app)/tasks/[taskId]/page.tsx +79 -0
- package/app/(auth)/forgot-password/page.tsx +106 -0
- package/app/(auth)/guest/page.tsx +56 -0
- package/app/(auth)/layout.tsx +13 -0
- package/app/(auth)/login/page.tsx +14 -0
- package/app/(auth)/register/page.tsx +193 -0
- package/app/(auth)/reset-password/page.tsx +138 -0
- package/app/api/account/claim/route.tsx +135 -0
- package/app/api/admin/backfill-encryption/route.tsx +43 -0
- package/app/api/admin/license/route.tsx +42 -0
- package/app/api/auth/2fa/route.tsx +148 -0
- package/app/api/auth/[...nextauth]/route.tsx +3 -0
- package/app/api/auth/change-password/route.tsx +61 -0
- package/app/api/auth/check-2fa/route.tsx +19 -0
- package/app/api/auth/forgot-password/route.tsx +65 -0
- package/app/api/auth/reset-password/route.tsx +52 -0
- package/app/api/auth/verify-2fa/route.tsx +88 -0
- package/app/api/backup/download/db/route.ts +29 -0
- package/app/api/backup/download/notes/route.ts +25 -0
- package/app/api/backup/settings/route.ts +92 -0
- package/app/api/billing/checkout/route.tsx +81 -0
- package/app/api/billing/migrate/route.tsx +163 -0
- package/app/api/billing/portal/route.tsx +24 -0
- package/app/api/billing/setup-intent/route.tsx +55 -0
- package/app/api/billing/status/route.tsx +36 -0
- package/app/api/billing/subscribe/route.tsx +85 -0
- package/app/api/billing/webhook/route.tsx +199 -0
- package/app/api/calendar-feeds/[feedId]/route.tsx +67 -0
- package/app/api/calendar-feeds/[feedId]/sync/route.tsx +37 -0
- package/app/api/calendar-feeds/events/route.tsx +82 -0
- package/app/api/calendar-feeds/route.tsx +52 -0
- package/app/api/calendar-feeds/sync-all/route.tsx +34 -0
- package/app/api/cron/calendar-feeds/route.tsx +31 -0
- package/app/api/cron/stale-tasks/route.tsx +51 -0
- package/app/api/cron/sync/route.tsx +34 -0
- package/app/api/devices/[deviceId]/route.tsx +25 -0
- package/app/api/devices/route.tsx +41 -0
- package/app/api/export/route.tsx +40 -0
- package/app/api/feedback/route.tsx +54 -0
- package/app/api/folders/[folderId]/route.tsx +51 -0
- package/app/api/folders/route.tsx +37 -0
- package/app/api/graph/route.tsx +242 -0
- package/app/api/guest/route.tsx +58 -0
- package/app/api/health/route.tsx +10 -0
- package/app/api/holidays/countries/route.tsx +14 -0
- package/app/api/holidays/route.tsx +49 -0
- package/app/api/holidays/states/route.tsx +21 -0
- package/app/api/invites/[token]/route.tsx +131 -0
- package/app/api/invites/route.tsx +74 -0
- package/app/api/mcp/generate-token/route.tsx +55 -0
- package/app/api/mcp/revoke-token/[tokenId]/route.tsx +30 -0
- package/app/api/mcp/update-alias/[tokenId]/route.tsx +22 -0
- package/app/api/notes/[noteId]/export/route.tsx +45 -0
- package/app/api/notes/[noteId]/route.tsx +360 -0
- package/app/api/notes/route.tsx +112 -0
- package/app/api/notifications/route.tsx +44 -0
- package/app/api/register/route.tsx +67 -0
- package/app/api/restore/route.tsx +148 -0
- package/app/api/sync/conflicts/[conflictId]/route.tsx +134 -0
- package/app/api/sync/conflicts/route.tsx +48 -0
- package/app/api/sync/status/route.tsx +49 -0
- package/app/api/sync/trigger/route.tsx +15 -0
- package/app/api/tasks/[taskId]/detail/route.tsx +68 -0
- package/app/api/tasks/[taskId]/route.tsx +259 -0
- package/app/api/tasks/bulk/route.tsx +133 -0
- package/app/api/tasks/route.tsx +36 -0
- package/app/api/workspace/active/route.tsx +39 -0
- package/app/api/workspace/create-team/route.tsx +42 -0
- package/app/api/workspace/kanban-statuses/route.tsx +71 -0
- package/app/api/workspace/members/[memberId]/route.tsx +69 -0
- package/app/api/workspace/route.tsx +24 -0
- package/app/download/page.tsx +170 -0
- package/app/favicon.ico +0 -0
- package/app/generated/prisma/client.d.ts +1 -0
- package/app/generated/prisma/client.js +5 -0
- package/app/generated/prisma/default.d.ts +1 -0
- package/app/generated/prisma/default.js +5 -0
- package/app/generated/prisma/edge.d.ts +1 -0
- package/app/generated/prisma/edge.js +497 -0
- package/app/generated/prisma/index-browser.js +523 -0
- package/app/generated/prisma/index.d.ts +46376 -0
- package/app/generated/prisma/index.js +497 -0
- package/app/generated/prisma/package.json +144 -0
- package/app/generated/prisma/query_compiler_fast_bg.js +2 -0
- package/app/generated/prisma/query_compiler_fast_bg.wasm +0 -0
- package/app/generated/prisma/query_compiler_fast_bg.wasm-base64.js +2 -0
- package/app/generated/prisma/runtime/client.d.ts +3386 -0
- package/app/generated/prisma/runtime/client.js +86 -0
- package/app/generated/prisma/runtime/index-browser.d.ts +90 -0
- package/app/generated/prisma/runtime/index-browser.js +6 -0
- package/app/generated/prisma/runtime/wasm-compiler-edge.js +76 -0
- package/app/generated/prisma/schema.prisma +456 -0
- package/app/generated/prisma/wasm-edge-light-loader.mjs +5 -0
- package/app/generated/prisma/wasm-worker-loader.mjs +5 -0
- package/app/globals.css +54 -0
- package/app/invite/[token]/page.tsx +52 -0
- package/app/layout.tsx +90 -0
- package/app/mcp/route.tsx +430 -0
- package/app/opengraph-image.tsx +120 -0
- package/app/page.tsx +398 -0
- package/app/privacy/page.tsx +69 -0
- package/app/robots.tsx +25 -0
- package/app/sitemap.tsx +36 -0
- package/app/terms/page.tsx +69 -0
- package/app/upgrade/page.tsx +75 -0
- package/auth.config.ts +33 -0
- package/auth.ts +79 -0
- package/bin/brief.js +229 -0
- package/components/auth/login-form.tsx +302 -0
- package/components/auth/password-checklist.tsx +31 -0
- package/components/auth/password-input.tsx +36 -0
- package/components/auth/switch-account-button.test.tsx +22 -0
- package/components/auth/switch-account-button.tsx +19 -0
- package/components/auth/two-factor-input.tsx +116 -0
- package/components/billing/billing-dashboard.tsx +265 -0
- package/components/billing/card-form.tsx +210 -0
- package/components/billing/claim-account-form.tsx +99 -0
- package/components/branding/app-logo.test.tsx +20 -0
- package/components/branding/app-logo.tsx +25 -0
- package/components/calendar/calendar-agenda.tsx +150 -0
- package/components/calendar/calendar-drag.test.tsx +177 -0
- package/components/calendar/calendar-grid.tsx +357 -0
- package/components/calendar/calendar-hooks.test.tsx +27 -0
- package/components/calendar/calendar-hooks.ts +351 -0
- package/components/calendar/calendar-toolbar.test.tsx +68 -0
- package/components/calendar/calendar-toolbar.tsx +291 -0
- package/components/calendar/calendar-types.ts +148 -0
- package/components/calendar/calendar-view.test.tsx +295 -0
- package/components/calendar/calendar-view.tsx +307 -0
- package/components/calendar/day-detail-popover.tsx +174 -0
- package/components/calendar/task-chip.tsx +86 -0
- package/components/command/command-palette.test.tsx +33 -0
- package/components/command/command-palette.tsx +310 -0
- package/components/download-cta.tsx +87 -0
- package/components/feedback/feedback-popup.tsx +207 -0
- package/components/graph/graph-draw.ts +337 -0
- package/components/graph/graph-overlays.tsx +160 -0
- package/components/graph/graph-page.test.tsx +131 -0
- package/components/graph/graph-page.tsx +263 -0
- package/components/graph/graph-types.ts +47 -0
- package/components/graph/graph-view.tsx +322 -0
- package/components/guide/guide-view.tsx +522 -0
- package/components/kanban/kanban-board.test.tsx +128 -0
- package/components/kanban/kanban-board.tsx +361 -0
- package/components/kanban/kanban-card-menu.tsx +102 -0
- package/components/kanban/kanban-card.tsx +227 -0
- package/components/kanban/kanban-column.tsx +49 -0
- package/components/kanban/kanban-status-context.tsx +28 -0
- package/components/landing/calendar-sandbox.test.tsx +15 -0
- package/components/landing/calendar-sandbox.tsx +107 -0
- package/components/landing/graph-sandbox.test.tsx +27 -0
- package/components/landing/graph-sandbox.tsx +80 -0
- package/components/landing/kanban-sandbox.test.tsx +24 -0
- package/components/landing/kanban-sandbox.tsx +101 -0
- package/components/landing/landing-showcase.test.tsx +21 -0
- package/components/landing/landing-showcase.tsx +54 -0
- package/components/landing/list-sandbox.tsx +86 -0
- package/components/landing/mock-workspace.ts +168 -0
- package/components/landing/notes-sandbox.test.tsx +14 -0
- package/components/landing/notes-sandbox.tsx +88 -0
- package/components/layout/app-shell.tsx +83 -0
- package/components/layout/backup-scheduler.tsx +122 -0
- package/components/layout/bottom-nav.tsx +43 -0
- package/components/layout/icon-bar.test.tsx +29 -0
- package/components/layout/icon-bar.tsx +118 -0
- package/components/layout/mobile-top-bar.tsx +68 -0
- package/components/layout/notes-panel-folder.tsx +127 -0
- package/components/layout/notes-panel-note-item.tsx +140 -0
- package/components/layout/notes-panel-task-tab.tsx +63 -0
- package/components/layout/notes-panel-types.ts +44 -0
- package/components/layout/notes-panel.tsx +476 -0
- package/components/layout/notification-bell.tsx +251 -0
- package/components/layout/paywall-screen.tsx +41 -0
- package/components/layout/pro-banner.tsx +76 -0
- package/components/layout/sw-register.tsx +27 -0
- package/components/layout/workspace-switcher.tsx +90 -0
- package/components/notes/mobile-bottom-sheet.tsx +99 -0
- package/components/notes/note-editor-context-menu.tsx +47 -0
- package/components/notes/note-editor-dom.ts +33 -0
- package/components/notes/note-editor-dropdowns.tsx +484 -0
- package/components/notes/note-editor-hooks.ts +692 -0
- package/components/notes/note-editor-keyboard.ts +305 -0
- package/components/notes/note-editor-overlay.tsx +90 -0
- package/components/notes/note-editor.test.tsx +372 -0
- package/components/notes/note-editor.tsx +662 -0
- package/components/notes/note-preview-pane.tsx +156 -0
- package/components/notes/note-tabs.tsx +120 -0
- package/components/notes/note-types.tsx +157 -0
- package/components/settings/accept-invite.tsx +108 -0
- package/components/settings/agent-token-settings.tsx +369 -0
- package/components/settings/backup-restore-settings.test.tsx +25 -0
- package/components/settings/backup-restore-settings.tsx +327 -0
- package/components/settings/calendar-feeds-settings.tsx +489 -0
- package/components/settings/calendar-general-settings.tsx +174 -0
- package/components/settings/confirm-danger-action.test.tsx +215 -0
- package/components/settings/confirm-danger-action.tsx +65 -0
- package/components/settings/security-settings.tsx +252 -0
- package/components/settings/settings-guidance.test.tsx +98 -0
- package/components/settings/team-settings.tsx +319 -0
- package/components/settings/two-factor-auth.tsx +296 -0
- package/components/settings/workspace-settings-client.tsx +363 -0
- package/components/settings/workspace-settings-form.tsx +73 -0
- package/components/sync/conflict-viewer.tsx +247 -0
- package/components/sync/sync-indicator.tsx +171 -0
- package/components/tasks/snippet-thread.tsx +119 -0
- package/components/tasks/status-dot.tsx +47 -0
- package/components/tasks/task-badge.tsx +43 -0
- package/components/tasks/task-detail.test.tsx +187 -0
- package/components/tasks/task-detail.tsx +458 -0
- package/components/tasks/task-list-filters.test.tsx +75 -0
- package/components/tasks/task-list-filters.tsx +163 -0
- package/components/tasks/task-list-types.ts +20 -0
- package/components/tasks/task-list.test.tsx +175 -0
- package/components/tasks/task-list.tsx +481 -0
- package/components/tasks/task-row.tsx +85 -0
- package/components/tasks/task-table-row.tsx +259 -0
- package/components/ui/skeleton.tsx +3 -0
- package/components/ui/toast.test.tsx +42 -0
- package/components/ui/toast.tsx +70 -0
- package/instrumentation.tsx +23 -0
- package/lib/api-error.ts +50 -0
- package/lib/backup/backup-runner.test.ts +32 -0
- package/lib/backup/backup-runner.ts +19 -0
- package/lib/backup/backup-schedule.test.ts +23 -0
- package/lib/backup/backup-schedule.ts +55 -0
- package/lib/backup/backup-settings.test.ts +30 -0
- package/lib/backup/backup-settings.ts +27 -0
- package/lib/backup/export-notes-zip.test.ts +26 -0
- package/lib/backup/export-notes-zip.ts +82 -0
- package/lib/backup/export-workspace-backup.test.ts +17 -0
- package/lib/backup/export-workspace-backup.ts +77 -0
- package/lib/backup/restore-workspace-from-export.test.ts +18 -0
- package/lib/backup/restore-workspace-from-export.ts +183 -0
- package/lib/backup/types.ts +14 -0
- package/lib/brand-icons.ts +1 -0
- package/lib/calendar-feed-crypto.ts +38 -0
- package/lib/calendar-feed.ts +239 -0
- package/lib/client/online-status.ts +47 -0
- package/lib/conflict-resolver.test.ts +57 -0
- package/lib/conflict-resolver.ts +240 -0
- package/lib/db-init.ts +79 -0
- package/lib/email.ts +159 -0
- package/lib/encryption.test.ts +41 -0
- package/lib/encryption.ts +98 -0
- package/lib/extract-snippet.test.ts +123 -0
- package/lib/extract-snippet.ts +69 -0
- package/lib/kanban-status.ts +55 -0
- package/lib/license.ts +21 -0
- package/lib/limits.ts +31 -0
- package/lib/mcp-auth.test.ts +58 -0
- package/lib/mcp-auth.ts +65 -0
- package/lib/mcp-contract.test.ts +25 -0
- package/lib/mcp-contract.ts +210 -0
- package/lib/mcp-handler.ts +31 -0
- package/lib/mcp-url.test.ts +12 -0
- package/lib/mcp-url.ts +7 -0
- package/lib/mentions.test.ts +45 -0
- package/lib/mentions.ts +73 -0
- package/lib/note-crypto.ts +108 -0
- package/lib/note-sync.ts +201 -0
- package/lib/note-title.ts +93 -0
- package/lib/prisma.ts +193 -0
- package/lib/pro-flush.ts +292 -0
- package/lib/rate-limit.ts +57 -0
- package/lib/stripe.ts +38 -0
- package/lib/sync-worker.ts +388 -0
- package/lib/task-parser.test.ts +91 -0
- package/lib/task-parser.ts +81 -0
- package/lib/task-utils.ts +52 -0
- package/lib/use-is-electron.ts +19 -0
- package/lib/use-is-mobile.ts +22 -0
- package/lib/validation/calendar-feed.ts +31 -0
- package/lib/validation/note.ts +27 -0
- package/lib/validation/task.ts +26 -0
- package/lib/view-preferences.test.ts +54 -0
- package/lib/view-preferences.ts +28 -0
- package/lib/workspace.ts +66 -0
- package/next.config.ts +21 -0
- package/package.json +54 -3
- package/postcss.config.mjs +7 -0
- package/prisma/migrations/20260519021916_init/migration.sql +388 -0
- package/prisma/migrations/20260519061113_drop_sync_password/migration.sql +8 -0
- package/prisma/migrations/20260520065016_add_task_start_date/migration.sql +2 -0
- package/prisma/migrations/20260529010600_remove_encryption_fields/migration.sql +12 -0
- package/prisma/migrations/20260529020000_restore_encryption_salt/migration.sql +3 -0
- package/prisma/migrations/20260529030000_add_folders/migration.sql +17 -0
- package/prisma/migrations/20260605000000_deferred_fixes/migration.sql +31 -0
- package/prisma/migrations/20260605020806_add_pending_sync_to_note_and_task/migration.sql +5 -0
- package/prisma/migrations/20260605063634_add_stripe_webhook_event_sync_lock/migration.sql +14 -0
- package/prisma/migrations/20260605100000_add_prod_indexes/migration.sql +26 -0
- package/prisma/migrations/20260608081404_add_kanban_statuses/migration.sql +23 -0
- package/prisma/migrations/20260611032723_add_calendar_feeds/migration.sql +43 -0
- package/prisma/migrations/20260611040000_add_calendar_feed_color/migration.sql +2 -0
- package/prisma/migrations/20260611050000_add_task_priority/migration.sql +14 -0
- package/prisma/migrations/20260612060000_add_critical_priority/migration.sql +2 -0
- package/prisma/migrations/20260613090000_add_backup_settings/migration.sql +25 -0
- package/prisma/migrations/20260614160000_add_feedback/migration.sql +20 -0
- package/prisma/migrations/20260614210000_add_2fa/migration.sql +4 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +457 -0
- package/public/Logo_icon.svg +1 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/icon_dark.svg +1 -0
- package/public/knotpad_icon.svg +1 -0
- package/public/knotpad_logo_full.svg +1 -0
- package/public/manifest.json +14 -0
- package/public/next.svg +1 -0
- package/public/sw.js +137 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/tsconfig.json +35 -0
- package/brief.js +0 -311
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { auth } from "@/auth";
|
|
2
|
+
import { redirect } from "next/navigation";
|
|
3
|
+
import { prisma } from "@/lib/prisma";
|
|
4
|
+
import { getActiveWorkspaceId } from "@/lib/workspace";
|
|
5
|
+
|
|
6
|
+
export default async function NotesHome() {
|
|
7
|
+
const session = await auth();
|
|
8
|
+
if (!session) redirect("/login");
|
|
9
|
+
|
|
10
|
+
const workspaceId = await getActiveWorkspaceId(session.user.id);
|
|
11
|
+
const latestNote = workspaceId
|
|
12
|
+
? await prisma.note.findFirst({
|
|
13
|
+
where: { workspaceId },
|
|
14
|
+
orderBy: { updatedAt: "desc" },
|
|
15
|
+
select: { id: true },
|
|
16
|
+
})
|
|
17
|
+
: null;
|
|
18
|
+
if (latestNote) redirect(`/notes/${latestNote.id}`);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="flex flex-1 flex-col items-center justify-center gap-4 text-center px-6">
|
|
22
|
+
<div className="max-w-xs space-y-2">
|
|
23
|
+
<h2 className="text-lg font-semibold text-zinc-300">No notes yet</h2>
|
|
24
|
+
<p className="text-sm text-zinc-500">
|
|
25
|
+
Click the{" "}
|
|
26
|
+
<span className="font-mono text-xs bg-zinc-800 px-1.5 py-0.5 rounded text-zinc-400">
|
|
27
|
+
+
|
|
28
|
+
</span>{" "}
|
|
29
|
+
button in the panel to create your first note.
|
|
30
|
+
</p>
|
|
31
|
+
<p className="text-xs text-zinc-600 mt-3">
|
|
32
|
+
Write{" "}
|
|
33
|
+
<code className="bg-zinc-800 px-1 rounded text-zinc-400">- [ ] task @you</code>{" "}
|
|
34
|
+
inside a note to create tasks automatically.
|
|
35
|
+
</p>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { auth } from "@/auth";
|
|
2
|
+
import { redirect } from "next/navigation";
|
|
3
|
+
import { prisma } from "@/lib/prisma";
|
|
4
|
+
import { AgentTokenSettings } from "@/components/settings/agent-token-settings";
|
|
5
|
+
import { getMcpUrl } from "@/lib/mcp-url";
|
|
6
|
+
|
|
7
|
+
export default async function AgentTokenPage() {
|
|
8
|
+
const session = await auth();
|
|
9
|
+
if (!session) redirect("/login");
|
|
10
|
+
|
|
11
|
+
const memberships = await prisma.workspaceMember.findMany({
|
|
12
|
+
where: { userId: session.user.id, revokedAt: null },
|
|
13
|
+
include: { workspace: true },
|
|
14
|
+
orderBy: { joinedAt: "asc" },
|
|
15
|
+
});
|
|
16
|
+
if (!memberships.length) redirect("/login");
|
|
17
|
+
|
|
18
|
+
const tokens = await prisma.mcpToken.findMany({
|
|
19
|
+
where: {
|
|
20
|
+
userId: session.user.id,
|
|
21
|
+
workspaceId: { in: memberships.map((m) => m.workspaceId) },
|
|
22
|
+
revokedAt: null,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const tokenByWorkspace = Object.fromEntries(tokens.map((t) => [t.workspaceId, t]));
|
|
27
|
+
|
|
28
|
+
const workspaces = memberships.map((m) => {
|
|
29
|
+
const t = tokenByWorkspace[m.workspaceId] ?? null;
|
|
30
|
+
return {
|
|
31
|
+
id: m.workspaceId,
|
|
32
|
+
name: m.workspace.name,
|
|
33
|
+
slug: m.workspace.slug,
|
|
34
|
+
existingToken: t
|
|
35
|
+
? { id: t.id, alias: t.alias, lastUsed: t.lastUsed?.toISOString() ?? null }
|
|
36
|
+
: null,
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000";
|
|
41
|
+
const mcpUrl = getMcpUrl(appUrl, process.env.NEXT_PUBLIC_MCP_URL);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className="max-w-2xl space-y-6">
|
|
45
|
+
<div>
|
|
46
|
+
<h2 className="text-base font-semibold text-zinc-200">Agent Token</h2>
|
|
47
|
+
<p className="text-sm text-zinc-500 mt-1">
|
|
48
|
+
Generate a personal MCP token, then reuse the setup templates below whenever you reconnect an AI agent.
|
|
49
|
+
</p>
|
|
50
|
+
</div>
|
|
51
|
+
<AgentTokenSettings
|
|
52
|
+
workspaces={workspaces}
|
|
53
|
+
mcpUrl={mcpUrl}
|
|
54
|
+
appUrl={appUrl}
|
|
55
|
+
userId={session.user.id}
|
|
56
|
+
/>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { auth } from "@/auth";
|
|
2
|
+
import { redirect } from "next/navigation";
|
|
3
|
+
import { prisma } from "@/lib/prisma";
|
|
4
|
+
import { BackupRestoreSettings } from "@/components/settings/backup-restore-settings";
|
|
5
|
+
|
|
6
|
+
export default async function BackupPage() {
|
|
7
|
+
const session = await auth();
|
|
8
|
+
if (!session) redirect("/login");
|
|
9
|
+
|
|
10
|
+
const member = await prisma.workspaceMember.findFirst({
|
|
11
|
+
where: { userId: session.user.id, revokedAt: null },
|
|
12
|
+
include: {
|
|
13
|
+
workspace: {
|
|
14
|
+
include: {
|
|
15
|
+
backupSettings: true,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
if (!member) redirect("/login");
|
|
21
|
+
|
|
22
|
+
const initialSettings = member.workspace.backupSettings
|
|
23
|
+
? {
|
|
24
|
+
scheduleEnabled: member.workspace.backupSettings.scheduleEnabled,
|
|
25
|
+
scheduleCadence: member.workspace.backupSettings.scheduleCadence,
|
|
26
|
+
destinationPath: member.workspace.backupSettings.destinationPath,
|
|
27
|
+
includeMarkdownZip: member.workspace.backupSettings.includeMarkdownZip,
|
|
28
|
+
lastBackupAt: member.workspace.backupSettings.lastBackupAt?.toISOString() ?? null,
|
|
29
|
+
lastBackupStatus: member.workspace.backupSettings.lastBackupStatus,
|
|
30
|
+
lastBackupError: member.workspace.backupSettings.lastBackupError,
|
|
31
|
+
}
|
|
32
|
+
: null;
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="max-w-2xl space-y-6">
|
|
36
|
+
<div>
|
|
37
|
+
<h2 className="text-base font-semibold text-zinc-200">Backup & Restore</h2>
|
|
38
|
+
<p className="mt-1 text-sm text-zinc-500">
|
|
39
|
+
Configure local backups, download exports, and restore from a backup file.
|
|
40
|
+
</p>
|
|
41
|
+
</div>
|
|
42
|
+
<BackupRestoreSettings
|
|
43
|
+
isCloudWorkspace={member.workspace.isCloud}
|
|
44
|
+
isOwner={member.role !== "MEMBER"}
|
|
45
|
+
initialSettings={initialSettings}
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { auth } from "@/auth";
|
|
2
|
+
import { redirect } from "next/navigation";
|
|
3
|
+
import { prisma } from "@/lib/prisma";
|
|
4
|
+
import { BillingDashboard } from "@/components/billing/billing-dashboard";
|
|
5
|
+
|
|
6
|
+
export default async function BillingPage() {
|
|
7
|
+
const session = await auth();
|
|
8
|
+
if (!session) redirect("/login");
|
|
9
|
+
|
|
10
|
+
const member = await prisma.workspaceMember.findFirst({
|
|
11
|
+
where: { userId: session.user.id, revokedAt: null },
|
|
12
|
+
include: {
|
|
13
|
+
workspace: {
|
|
14
|
+
include: { _count: { select: { members: { where: { revokedAt: null } } } } },
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
orderBy: { joinedAt: "asc" },
|
|
18
|
+
});
|
|
19
|
+
if (!member) redirect("/login");
|
|
20
|
+
|
|
21
|
+
const { workspace } = member;
|
|
22
|
+
|
|
23
|
+
// A guest (local, no-password account) must create a real account before they
|
|
24
|
+
// can pay — surfaced as a claim step in the dashboard.
|
|
25
|
+
const me = await prisma.user.findUnique({
|
|
26
|
+
where: { id: session.user.id },
|
|
27
|
+
select: { email: true, passwordHash: true },
|
|
28
|
+
});
|
|
29
|
+
const isGuest = !!me?.email?.endsWith("@local.brief") && !me?.passwordHash;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="max-w-lg space-y-6">
|
|
33
|
+
<div>
|
|
34
|
+
<h2 className="text-base font-semibold text-zinc-200">Billing</h2>
|
|
35
|
+
<p className="text-sm text-zinc-500 mt-1">
|
|
36
|
+
Manage your subscription and seats.
|
|
37
|
+
</p>
|
|
38
|
+
</div>
|
|
39
|
+
<BillingDashboard
|
|
40
|
+
isPro={workspace.isPro}
|
|
41
|
+
isOwner={member.role === "OWNER"}
|
|
42
|
+
memberCount={workspace._count.members}
|
|
43
|
+
seatCount={workspace.seatCount}
|
|
44
|
+
stripeId={workspace.stripeId}
|
|
45
|
+
planType={workspace.planType}
|
|
46
|
+
licenseType={workspace.licenseType}
|
|
47
|
+
workspaceType={workspace.type}
|
|
48
|
+
workspaceId={workspace.id}
|
|
49
|
+
isGuest={isGuest}
|
|
50
|
+
/>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { auth } from "@/auth";
|
|
2
|
+
import { redirect } from "next/navigation";
|
|
3
|
+
import { prisma } from "@/lib/prisma";
|
|
4
|
+
import { CalendarFeedsSettings } from "@/components/settings/calendar-feeds-settings";
|
|
5
|
+
import { CalendarGeneralSettings } from "@/components/settings/calendar-general-settings";
|
|
6
|
+
|
|
7
|
+
export default async function CalendarSettingsPage() {
|
|
8
|
+
const session = await auth();
|
|
9
|
+
if (!session) redirect("/login");
|
|
10
|
+
|
|
11
|
+
const feeds = await prisma.calendarFeed.findMany({
|
|
12
|
+
where: { userId: session.user.id },
|
|
13
|
+
orderBy: { createdAt: "asc" },
|
|
14
|
+
select: { id: true, label: true, color: true, enabled: true, lastFetchedAt: true, lastError: true },
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className="max-w-lg space-y-8">
|
|
19
|
+
<div>
|
|
20
|
+
<h2 className="text-base font-semibold text-zinc-200">Calendars</h2>
|
|
21
|
+
<p className="text-sm text-zinc-500 mt-1">
|
|
22
|
+
Connect read-only calendar subscriptions (Google, Apple, etc.) to see your busy times
|
|
23
|
+
on the Knotpad calendar. This is import-only — Knotpad never edits these calendars.
|
|
24
|
+
</p>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<CalendarGeneralSettings />
|
|
28
|
+
|
|
29
|
+
<CalendarFeedsSettings
|
|
30
|
+
feeds={feeds.map((f) => ({
|
|
31
|
+
id: f.id,
|
|
32
|
+
label: f.label,
|
|
33
|
+
color: f.color,
|
|
34
|
+
enabled: f.enabled,
|
|
35
|
+
lastFetchedAt: f.lastFetchedAt?.toISOString() ?? null,
|
|
36
|
+
lastError: f.lastError,
|
|
37
|
+
}))}
|
|
38
|
+
/>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
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 SettingsLayout from "@/app/(app)/settings/layout";
|
|
6
|
+
|
|
7
|
+
const { mockUsePathname } = vi.hoisted(() => ({
|
|
8
|
+
mockUsePathname: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock("next/navigation", () => ({
|
|
12
|
+
usePathname: mockUsePathname,
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
describe("SettingsLayout", () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
mockUsePathname.mockReset();
|
|
18
|
+
mockUsePathname.mockReturnValue("/settings/workspace");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
cleanup();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("filters visible settings tabs from the search field", async () => {
|
|
26
|
+
const user = userEvent.setup();
|
|
27
|
+
|
|
28
|
+
render(
|
|
29
|
+
<SettingsLayout>
|
|
30
|
+
<div>Body</div>
|
|
31
|
+
</SettingsLayout>
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
await user.type(screen.getByPlaceholderText("Search settings"), "agent");
|
|
35
|
+
|
|
36
|
+
expect(screen.getByText("Agent Token")).toBeInTheDocument();
|
|
37
|
+
expect(screen.queryByText("Workspace")).not.toBeInTheDocument();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { usePathname } from "next/navigation";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
|
|
7
|
+
const tabs = [
|
|
8
|
+
{ href: "/settings/workspace", label: "Workspace" },
|
|
9
|
+
{ href: "/settings/team", label: "Team" },
|
|
10
|
+
{ href: "/settings/billing", label: "Billing" },
|
|
11
|
+
{ href: "/settings/security", label: "Security" },
|
|
12
|
+
{ href: "/settings/backup", label: "Backup & Restore" },
|
|
13
|
+
{ href: "/settings/calendar", label: "Calendars" },
|
|
14
|
+
{ href: "/settings/agent-token", label: "Agent Token" },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const descriptions: Record<string, string> = {
|
|
18
|
+
"/settings/workspace": "Rename the workspace, adjust defaults, and manage shared workspace behavior.",
|
|
19
|
+
"/settings/team": "Invite people, manage access, and keep ownership clear.",
|
|
20
|
+
"/settings/billing": "Review plan status, limits, and billing controls.",
|
|
21
|
+
"/settings/security": "Manage authentication and account security settings.",
|
|
22
|
+
"/settings/backup": "Export, restore, and protect your workspace data.",
|
|
23
|
+
"/settings/calendar": "Control calendars, feeds, and scheduling defaults.",
|
|
24
|
+
"/settings/agent-token": "Create and revoke agent access with clear operational consequences.",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default function SettingsLayout({ children }: { children: React.ReactNode }) {
|
|
28
|
+
const pathname = usePathname();
|
|
29
|
+
const [query, setQuery] = useState("");
|
|
30
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
31
|
+
const visibleTabs = tabs.filter((tab) => tab.label.toLowerCase().includes(normalizedQuery));
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className="flex flex-1 flex-col overflow-hidden">
|
|
35
|
+
<div className="border-b border-zinc-800 px-4 py-3 md:px-6">
|
|
36
|
+
<h1 className="text-sm font-semibold text-zinc-300">Settings</h1>
|
|
37
|
+
<p className="mt-1 max-w-2xl text-xs text-zinc-500">
|
|
38
|
+
{descriptions[pathname] ?? "Manage your workspace configuration."}
|
|
39
|
+
</p>
|
|
40
|
+
</div>
|
|
41
|
+
<div className="flex flex-1 flex-col overflow-hidden md:flex-row">
|
|
42
|
+
<nav className="min-w-0 shrink-0 border-b border-zinc-800 py-2 md:w-52 md:border-b-0 md:border-r md:py-3">
|
|
43
|
+
<div className="px-2 pb-2">
|
|
44
|
+
<input
|
|
45
|
+
value={query}
|
|
46
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
47
|
+
placeholder="Search settings"
|
|
48
|
+
className="w-full rounded-md border border-zinc-800 bg-zinc-950 px-3 py-2 text-sm text-zinc-300 outline-none placeholder:text-zinc-500"
|
|
49
|
+
/>
|
|
50
|
+
</div>
|
|
51
|
+
<div className="flex gap-1 overflow-x-auto px-2 [&::-webkit-scrollbar]:hidden md:flex-col md:space-y-0.5">
|
|
52
|
+
{visibleTabs.map((t) => (
|
|
53
|
+
<Link
|
|
54
|
+
key={t.href}
|
|
55
|
+
href={t.href}
|
|
56
|
+
className={`flex shrink-0 items-center whitespace-nowrap rounded-md px-3 py-2 text-sm transition-colors ${
|
|
57
|
+
pathname === t.href
|
|
58
|
+
? "bg-zinc-800 text-zinc-200"
|
|
59
|
+
: "text-zinc-400 hover:bg-zinc-800 hover:text-zinc-200"
|
|
60
|
+
}`}
|
|
61
|
+
>
|
|
62
|
+
{t.label}
|
|
63
|
+
</Link>
|
|
64
|
+
))}
|
|
65
|
+
</div>
|
|
66
|
+
</nav>
|
|
67
|
+
<div className="min-w-0 flex-1 overflow-y-auto px-4 py-5 md:px-8 md:py-6">{children}</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { auth } from "@/auth";
|
|
2
|
+
import { redirect } from "next/navigation";
|
|
3
|
+
import { prisma } from "@/lib/prisma";
|
|
4
|
+
import { SecuritySettings } from "@/components/settings/security-settings";
|
|
5
|
+
|
|
6
|
+
export default async function SecurityPage() {
|
|
7
|
+
const session = await auth();
|
|
8
|
+
if (!session) redirect("/login");
|
|
9
|
+
|
|
10
|
+
const member = await prisma.workspaceMember.findFirst({
|
|
11
|
+
where: { userId: session.user.id },
|
|
12
|
+
include: { workspace: true, user: { select: { passwordHash: true, twoFactorEnabled: true } } },
|
|
13
|
+
});
|
|
14
|
+
if (!member) redirect("/login");
|
|
15
|
+
|
|
16
|
+
const devices = await prisma.deviceSession.findMany({
|
|
17
|
+
where: { userId: session.user.id, revokedAt: null },
|
|
18
|
+
orderBy: { lastActive: "desc" },
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className="max-w-lg space-y-6">
|
|
23
|
+
<div>
|
|
24
|
+
<h2 className="text-base font-semibold text-zinc-200">Security</h2>
|
|
25
|
+
<p className="text-sm text-zinc-500 mt-1">
|
|
26
|
+
Manage connected devices, change your password, and restore from cloud.
|
|
27
|
+
</p>
|
|
28
|
+
</div>
|
|
29
|
+
<SecuritySettings
|
|
30
|
+
isPro={member.workspace.isPro}
|
|
31
|
+
isOwner={member.role === "OWNER"}
|
|
32
|
+
devices={devices.map((d) => ({
|
|
33
|
+
id: d.id,
|
|
34
|
+
deviceName: d.deviceName,
|
|
35
|
+
deviceId: d.deviceId,
|
|
36
|
+
lastActive: d.lastActive.toISOString(),
|
|
37
|
+
}))}
|
|
38
|
+
hasPassword={!!member.user.passwordHash}
|
|
39
|
+
twoFactorEnabled={member.user.twoFactorEnabled}
|
|
40
|
+
/>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { auth } from "@/auth";
|
|
2
|
+
import { redirect } from "next/navigation";
|
|
3
|
+
import { prisma } from "@/lib/prisma";
|
|
4
|
+
import { TeamSettings } from "@/components/settings/team-settings";
|
|
5
|
+
|
|
6
|
+
export default async function TeamSettingsPage() {
|
|
7
|
+
const session = await auth();
|
|
8
|
+
if (!session) redirect("/login");
|
|
9
|
+
|
|
10
|
+
const member = await prisma.workspaceMember.findFirst({
|
|
11
|
+
where: { userId: session.user.id, revokedAt: null },
|
|
12
|
+
include: {
|
|
13
|
+
workspace: {
|
|
14
|
+
include: {
|
|
15
|
+
members: { where: { revokedAt: null }, include: { user: true } },
|
|
16
|
+
invites: { where: { acceptedAt: null, expiresAt: { gt: new Date() } } },
|
|
17
|
+
mcpTokens: {
|
|
18
|
+
where: { revokedAt: null },
|
|
19
|
+
include: { user: { select: { id: true, name: true, email: true } } },
|
|
20
|
+
orderBy: { createdAt: "desc" },
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
if (!member) redirect("/login");
|
|
27
|
+
|
|
28
|
+
const members = member.workspace.members.map((m) => ({
|
|
29
|
+
id: m.id,
|
|
30
|
+
userId: m.userId,
|
|
31
|
+
name: m.user.name ?? m.user.email ?? "Unknown",
|
|
32
|
+
email: m.user.email ?? "",
|
|
33
|
+
role: m.role,
|
|
34
|
+
joinedAt: m.joinedAt.toISOString(),
|
|
35
|
+
isCurrentUser: m.userId === session.user.id,
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
const pendingInvites = member.workspace.invites.map((inv) => ({
|
|
39
|
+
id: inv.id,
|
|
40
|
+
email: inv.email,
|
|
41
|
+
role: inv.role,
|
|
42
|
+
token: inv.token,
|
|
43
|
+
expiresAt: inv.expiresAt.toISOString(),
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
const canManage = member.role === "OWNER" || member.role === "ADMIN";
|
|
47
|
+
|
|
48
|
+
const mcpTokens = member.workspace.mcpTokens.map((t) => ({
|
|
49
|
+
id: t.id,
|
|
50
|
+
alias: t.alias,
|
|
51
|
+
userName: t.user.name ?? t.user.email ?? "Unknown",
|
|
52
|
+
userId: t.userId,
|
|
53
|
+
lastUsed: t.lastUsed?.toISOString() ?? null,
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className="max-w-2xl space-y-8">
|
|
58
|
+
<div>
|
|
59
|
+
<h2 className="text-base font-semibold text-zinc-200">Team</h2>
|
|
60
|
+
<p className="text-sm text-zinc-500 mt-1">
|
|
61
|
+
Invite people and manage workspace access.
|
|
62
|
+
</p>
|
|
63
|
+
</div>
|
|
64
|
+
<TeamSettings
|
|
65
|
+
members={members}
|
|
66
|
+
pendingInvites={pendingInvites}
|
|
67
|
+
canManage={canManage}
|
|
68
|
+
currentUserId={session.user.id}
|
|
69
|
+
workspaceId={member.workspaceId}
|
|
70
|
+
mcpTokens={mcpTokens}
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { auth } from "@/auth";
|
|
2
|
+
import { redirect } from "next/navigation";
|
|
3
|
+
import { prisma } from "@/lib/prisma";
|
|
4
|
+
import { WorkspaceSettingsClient } from "@/components/settings/workspace-settings-client";
|
|
5
|
+
import { getKanbanStatuses } from "@/lib/kanban-status";
|
|
6
|
+
|
|
7
|
+
export default async function WorkspaceSettingsPage() {
|
|
8
|
+
const session = await auth();
|
|
9
|
+
if (!session) redirect("/login");
|
|
10
|
+
|
|
11
|
+
const member = await prisma.workspaceMember.findFirst({
|
|
12
|
+
where: { userId: session.user.id, revokedAt: null },
|
|
13
|
+
include: { workspace: true },
|
|
14
|
+
});
|
|
15
|
+
if (!member) redirect("/login");
|
|
16
|
+
|
|
17
|
+
const isOwnerOrAdmin = member.role === "OWNER" || member.role === "ADMIN";
|
|
18
|
+
const kanbanStatuses = await getKanbanStatuses(member.workspace.id);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<WorkspaceSettingsClient
|
|
22
|
+
workspace={member.workspace}
|
|
23
|
+
kanbanStatuses={kanbanStatuses}
|
|
24
|
+
canEdit={isOwnerOrAdmin}
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { auth } from "@/auth";
|
|
2
|
+
import { redirect, notFound } from "next/navigation";
|
|
3
|
+
import { prisma } from "@/lib/prisma";
|
|
4
|
+
import { decryptContent } from "@/lib/note-crypto";
|
|
5
|
+
import { getActiveWorkspaceId } from "@/lib/workspace";
|
|
6
|
+
import { extractSnippet } from "@/lib/extract-snippet";
|
|
7
|
+
import { TaskDetail } from "@/components/tasks/task-detail";
|
|
8
|
+
|
|
9
|
+
export default async function TaskDetailPage({
|
|
10
|
+
params,
|
|
11
|
+
searchParams,
|
|
12
|
+
}: {
|
|
13
|
+
params: Promise<{ taskId: string }>;
|
|
14
|
+
searchParams: Promise<{ from?: string }>;
|
|
15
|
+
}) {
|
|
16
|
+
const session = await auth();
|
|
17
|
+
if (!session) redirect("/login");
|
|
18
|
+
|
|
19
|
+
const { taskId } = await params;
|
|
20
|
+
const { from } = await searchParams;
|
|
21
|
+
const backHref = from ?? "/list";
|
|
22
|
+
|
|
23
|
+
const workspaceId = await getActiveWorkspaceId(session.user.id);
|
|
24
|
+
if (!workspaceId) redirect("/login");
|
|
25
|
+
|
|
26
|
+
const task = await prisma.task.findFirst({
|
|
27
|
+
where: { id: taskId, workspaceId },
|
|
28
|
+
include: {
|
|
29
|
+
note: { select: { id: true, title: true, content: true } },
|
|
30
|
+
assignee: { select: { id: true, name: true, email: true, image: true } },
|
|
31
|
+
auditLogs: {
|
|
32
|
+
orderBy: { createdAt: "desc" },
|
|
33
|
+
take: 20,
|
|
34
|
+
include: { user: { select: { id: true, name: true } } },
|
|
35
|
+
},
|
|
36
|
+
references: {
|
|
37
|
+
include: { note: { select: { id: true, title: true, content: true } } },
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (!task) notFound();
|
|
43
|
+
|
|
44
|
+
const noteContent = await decryptContent(task.note.content, workspaceId);
|
|
45
|
+
const snippet = extractSnippet(noteContent, task.title);
|
|
46
|
+
const refNotes = await Promise.all(
|
|
47
|
+
task.references
|
|
48
|
+
.filter((r) => r.noteId !== task.noteId)
|
|
49
|
+
.map(async (r) => {
|
|
50
|
+
const refContent = await decryptContent(r.note.content, workspaceId);
|
|
51
|
+
return {
|
|
52
|
+
noteId: r.noteId,
|
|
53
|
+
noteTitle: r.note.title,
|
|
54
|
+
snippet: r.snippet || extractSnippet(refContent, task.title),
|
|
55
|
+
};
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
const references = [
|
|
59
|
+
{ noteId: task.note.id, noteTitle: task.note.title, snippet },
|
|
60
|
+
...refNotes,
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const taskData = {
|
|
64
|
+
...task,
|
|
65
|
+
priority: task.priority.toLowerCase(),
|
|
66
|
+
note: { id: task.note.id, title: task.note.title },
|
|
67
|
+
startDate: task.startDate?.toISOString() ?? null,
|
|
68
|
+
dueDate: task.dueDate?.toISOString() ?? null,
|
|
69
|
+
createdAt: task.createdAt.toISOString(),
|
|
70
|
+
updatedAt: task.updatedAt.toISOString(),
|
|
71
|
+
auditLogs: task.auditLogs.map((l) => ({
|
|
72
|
+
...l,
|
|
73
|
+
createdAt: l.createdAt.toISOString(),
|
|
74
|
+
})),
|
|
75
|
+
references,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return <TaskDetail task={taskData} backHref={backHref} />;
|
|
79
|
+
}
|