Skip to content

Comments

feat: Decision Export System with Multiple Formats#3

Open
nikman21 wants to merge 1 commit intomainfrom
feature/export-system
Open

feat: Decision Export System with Multiple Formats#3
nikman21 wants to merge 1 commit intomainfrom
feature/export-system

Conversation

@nikman21
Copy link
Contributor

@nikman21 nikman21 commented Feb 7, 2026

What Was Built

A comprehensive Decision Export System for PM Analyzer that enables product managers to export decisions in multiple formats for different downstream tools.

Components Created

  1. DecisionExportDialog.tsx - A reusable export modal component

    • 7 export formats: Cursor/Claude Code, Linear, Jira, Slack, Notion, Markdown, JSON
    • Live preview of export format
    • Copy to clipboard functionality
    • Download as file
  2. Export API Route (/api/projects/[id]/decisions/[decisionId]/export)

    • GET endpoint for programmatic export
    • Supports all 7 formats
    • Returns appropriate Content-Type headers
  3. Decision Detail Page (/projects/[id]/decisions/[decisionId])

    • Full decision view with all metadata
    • Embedded Export dialog
    • Confidence score badges
    • Linked feedback display
  4. Decisions List Page (/projects/[id]/decisions)

    • All decisions in one place
    • Export button on each decision card

Export Formats

Format Use Case
Cursor/Claude Code AI pair programming handoff
Linear Issue tracker format
Jira Enterprise issue tracking
Slack Stakeholder communication
Notion Documentation export
Markdown General documentation
JSON Programmatic access

Example: Cursor/Claude Code Export

## Feature: [title]

**Summary:** [summary]

**Confidence Score:** 85%

---

### Scope
[scope content]

### Acceptance Criteria
- [ ] 1
- [ ] 2

### Risks to Consider
[risks]

### Non-Goals (Out of Scope)
[non-goals]

Why This Matters

Product managers need to hand off decisions to:

  • Engineering teams (Linear/Jira)
  • AI coding assistants (Cursor)
  • Stakeholders (Slack/Notion)
  • Documentation systems (Markdown/JSON)

This feature bridges the gap between PM Analyzer's decision-making capabilities and the tools teams actually use to build software.

How to Test

  1. Navigate to a project with decisions
  2. Click on any decision
  3. Click the "Export" button in the top right
  4. Select a format from the dropdown
  5. Preview the export, then copy or download

Files Changed

  • components/DecisionExportDialog.tsx - New component (12KB)
  • app/api/projects/[id]/decisions/[decisionId]/export/route.ts - New API
  • app/projects/[id]/decisions/[decisionId]/page.tsx - Decision detail view
  • app/projects/[id]/decisions/page.tsx - Decisions list view

Nightly Build (2026-02-07) 🌙

Summary by CodeRabbit

  • New Features
    • Added dedicated decision detail page with comprehensive information display
    • Added decisions list page showing all project decisions with status badges and key metrics
    • Added multi-format export functionality supporting Cursor, Linear, Jira, Slack, Notion, Markdown, and JSON
    • Added export preview and download capabilities for decisions

## What Was Built

A comprehensive Decision Export System for PM Analyzer that enables
product managers to export decisions in multiple formats for different
downstream tools.

### Components Created

1. **DecisionExportDialog.tsx** - A reusable export modal component
   - 7 export formats: Cursor/Claude Code, Linear, Jira, Slack, Notion, Markdown, JSON
   - Live preview of export format
   - Copy to clipboard functionality
   - Download as file

2. **Export API Route** (`/api/projects/[id]/decisions/[decisionId]/export`)
   - GET endpoint for programmatic export
   - Supports all 7 formats
   - Returns appropriate Content-Type headers

3. **Decision Detail Page** (`/projects/[id]/decisions/[decisionId]`)
   - Full decision view with all metadata
   - Embedded Export dialog
   - Confidence score badges
   - Linked feedback display

4. **Decisions List Page** (`/projects/[id]/decisions`)
   - All decisions in one place
   - Export button on each decision card
   - Quick filtering by status

### Export Formats

| Format | Use Case |
|--------|----------|
| Cursor/Claude Code | AI pair programming handoff with structured prompts |
| Linear | Issue tracker format for sprint planning |
| Jira | Enterprise issue tracking |
| Slack | Stakeholder communication with emoji confidence |
| Notion | Documentation and wiki export |
| Markdown | General documentation |
| JSON | Programmatic access and integration |

### Why This Matters

Product managers need to hand off decisions to:
- Engineering teams (Linear/Jira)
- AI coding assistants (Cursor)
- Stakeholders (Slack/Notion)
- Documentation systems (Markdown/JSON)

This feature bridges the gap between PM Analyzer's decision-making
capabilities and the tools teams actually use to build software.

---

**Nightly Build (2026-02-07)**
@coderabbitai
Copy link

coderabbitai bot commented Feb 7, 2026

📝 Walkthrough

Walkthrough

This pull request introduces decision export functionality to the application. It adds a new API route handler for exporting decisions in multiple formats, two new pages for viewing decisions (list and detail views), and a reusable DecisionExportDialog component supporting seven export formats (Cursor, Linear, Jira, Slack, Notion, Markdown, and JSON).

Changes

Cohort / File(s) Summary
Export API Route
app/api/projects/[id]/decisions/[decisionId]/export/route.ts
New GET endpoint that exports a decision to various formats. Validates format parameter, fetches decision with Prisma, constructs export payload, applies format-specific transformation, and returns appropriately typed response (JSON or text with Content-Disposition header).
Decision Pages
app/projects/[id]/decisions/[decisionId]/page.tsx, app/projects/[id]/decisions/page.tsx
New server-side pages displaying decision list with filtering/creation actions and individual decision detail view. Both fetch project/decision data via Prisma, validate ownership, render status/confidence badges, and integrate DecisionExportDialog.
Export Dialog Component
components/DecisionExportDialog.tsx
New client-side React component providing format selection, content preview, copy-to-clipboard, and download functionality. Includes seven formatter functions (formatAsCursor, formatAsLinear, formatAsJira, formatAsSlack, formatAsNotion, formatAsMarkdown, formatAsJson), ExportFormat type definition, DecisionExportData interface, and format metadata configuration.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as DecisionExportDialog
    participant API as /api/.../export
    participant DB as Database

    User->>UI: Select export format
    Note over UI: Generate preview<br/>(client-side)
    UI-->>User: Display preview

    User->>UI: Click Download
    UI->>API: GET /export?format=markdown
    
    API->>DB: Fetch decision by ID
    DB-->>API: Decision data
    
    API->>API: formatDecision(data, format)
    Note over API: Apply format-specific<br/>transformation
    
    API-->>UI: Formatted content +<br/>Content-Disposition header
    
    UI-->>User: Download file
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

A rabbit hops through formats bright,
Decisions exported—markdown, JSON light!
From cursor leaps to Slack's embrace,
Seven formats share one place. 🐰📤
Export magic, hop by hop!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarizes the main change: adding a decision export system with support for multiple formats. It directly reflects the core feature introduced across the four new files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/export-system

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 17596b3c02

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +3 to +7
import {
formatDecision,
ExportFormat,
DecisionExportData,
} from "@/components/DecisionExportDialog";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Move export formatting logic out of client module

This API route imports formatDecision from components/DecisionExportDialog.tsx, but that file is explicitly marked with "use client"; server handlers cannot safely execute client-module exports, so calling formatDecision here can fail the export endpoint at runtime/build time instead of returning content. Extract the formatter/types into a shared server-safe utility and import that utility from both the dialog and this route.

Useful? React with 👍 / 👎.

Comment on lines +352 to +356
const handleExport = (format: ExportFormat) => {
const content = updatePreview(format);
if (onExport) {
onExport(format, content);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Implement actual download when no onExport callback is passed

The Download action only regenerates preview text and conditionally calls onExport; there is no fallback that writes a blob or triggers a file download. In this commit both call sites render DecisionExportDialog without an onExport prop, so clicking "Download" does nothing for users on the decisions list/detail pages.

Useful? React with 👍 / 👎.

Filter
</Button>
<Button asChild size="sm">
<Link href={`/projects/${projectId}/decisions/new`}>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Link New Decision button to an existing route

This primary CTA points to /projects/${projectId}/decisions/new, but this change only adds /decisions/page.tsx and /decisions/[decisionId]/page.tsx under that segment, so "new" resolves as a decisionId and leads to a not-found page. As shipped, users are sent to a dead-end when trying to create a decision from this screen.

Useful? React with 👍 / 👎.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@app/api/projects/`[id]/decisions/[decisionId]/export/route.ts:
- Around line 86-88: The filename creation uses raw user input (decision.title)
and must be sanitized before putting it in the Content-Disposition header;
update the code that builds filename (the filename const in route.ts) to (1)
normalize and trim the title, (2) remove or replace control characters like
newlines and quotes and any path separators, (3) restrict to a safe subset
(e.g., convert to ASCII-like slug with only letters, numbers, hyphens, and
underscores, collapsing consecutive non-safe chars), and (4) provide a safe
fallback (e.g., "decision") if the result is empty; finally ensure the header
uses an encoded filename fallback (RFC5987 / encodeURIComponent) when setting
Content-Disposition so non-ASCII characters are handled safely.

In `@app/projects/`[id]/decisions/[decisionId]/page.tsx:
- Line 62: The code uses decision.status.replace("_", " ") which only replaces
the first underscore; update the rendering logic that references
decision.status.replace to replace all underscores (e.g., use a global
replacement or split/join) so multi-underscore statuses like
"in_progress_review" become "in progress review". Ensure this change is applied
where decision.status.replace is used in the page.tsx render.

In `@app/projects/`[id]/decisions/page.tsx:
- Around line 54-57: The Filter button (the Button element rendering the Filter
icon in page.tsx) is non-functional; add an onClick handler to either toggle a
local filter UI state (e.g., a useState like showFilters and a handler such as
handleToggleFilters) or call the existing filter-opening function if one exists,
and ensure the Button's onClick wires to that handler (or remove the Button
entirely if filtering is not implemented) so the control is not misleading to
users.

In `@components/DecisionExportDialog.tsx`:
- Around line 380-381: The custom modal in DecisionExportDialog (the
isOpen-controlled overlay div) lacks standard accessibility: replace the
hand-rolled overlay and content with the Radix/Dialog (or the project's Dialog
primitive from components/ui) so you get built-in focus trap, Escape handling,
and return-focus behavior; ensure the dialog root is controlled by the existing
isOpen state and calls the component's onClose/close handler when closed, and
add aria attributes (role="dialog" / aria-modal="true" and aria-labelledby
pointing to the dialog title element) for the dialog content that previously
lived inside the fixed inset div.
- Around line 363-364: The custom trigger wrapper currently only does
setIsOpen(true) which both fails to call updatePreview (so the preview is empty)
and creates accessibility issues by using a plain <div>; change the trigger
handling to invoke updatePreview() before opening (call updatePreview() then
setIsOpen(true)) and make the trigger keyboard-accessible by forwarding proper
button semantics instead of a bare div — e.g., use React.cloneElement(trigger, {
onClick: () => { updatePreview(); setIsOpen(true); }, onKeyDown: (e) => { if
(e.key === 'Enter' || e.key === ' ') { e.preventDefault(); updatePreview();
setIsOpen(true); } }, role: 'button', tabIndex: 0 }) so the custom trigger
receives click/keyboard handlers, role="button" and tabIndex to match how the
default Button path triggers updatePreview and supports keyboard interaction.
- Around line 456-462: The Download button currently calls
handleExport(selectedFormat) which only invokes the optional onExport callback
and does nothing when onExport is not provided; implement fallback download
logic inside handleExport in DecisionExportDialog: when onExport is undefined,
generate the export payload (based on selectedFormat and existing export
data/helpers in the component), create a Blob, create an object URL,
programmatically create and click an anchor (<a>) with download attribute, and
revoke the URL after use so the file actually downloads; keep the existing
onExport call path intact and only run the Blob/link code as the fallback.
🧹 Nitpick comments (5)
app/api/projects/[id]/decisions/[decisionId]/export/route.ts (3)

3-7: Importing formatting utilities from a "use client" component into a server route is an architectural smell.

The formatter functions (formatDecision, etc.) are pure logic with no React/browser dependencies. They should live in a shared utility module (e.g., lib/export-formatters.ts) and be imported by both the client component and the API route. This avoids coupling server code to client component boundaries and makes the formatters independently testable.


16-16: Unsafe type assertion before validation.

The as ExportFormat cast on an unvalidated query param defeats TypeScript's type safety. The validation on line 28 catches invalid values at runtime, but between lines 16–28 the variable is incorrectly typed.

Proposed fix
-    const format = (searchParams.get("format") as ExportFormat) || "markdown";
+    const format = searchParams.get("format") || "markdown";
 
     // Validate format
     const validFormats = [
       "cursor",
       "linear",
       "jira",
       "slack",
       "notion",
       "markdown",
       "json",
-    ];
-    if (!validFormats.includes(format)) {
+    ] as const;
+    if (!validFormats.includes(format as ExportFormat)) {
       return NextResponse.json(
         { error: `Invalid format. Valid formats: ${validFormats.join(", ")}` },
         { status: 400 }
       );
     }
+    const validatedFormat = format as ExportFormat;

Then use validatedFormat in subsequent code.


75-81: Double JSON serialize/parse is unnecessary.

formatAsJson returns JSON.stringify(...), then line 76 does JSON.parse(content) only to have NextResponse.json() re-stringify it. You can return the string directly.

Proposed fix
     if (format === "json") {
-      return NextResponse.json(JSON.parse(content), {
-        headers: {
-          "Content-Type": "application/json",
-        },
+      return new NextResponse(content, {
+        headers: {
+          "Content-Type": "application/json",
+        },
       });
     }
app/projects/[id]/decisions/page.tsx (1)

86-97: DecisionExportData construction is duplicated across three files.

The same field mapping from a Prisma decision to DecisionExportData appears here, in the detail page, and in the API route. Extract a helper (e.g., toDecisionExportData(decision, projectName)) into a shared module to reduce duplication and prevent drift.

Also applies to: 57-68

components/DecisionExportDialog.tsx (1)

279-298: JSON export omits projectName and linkedFeedbackIds count context.

formatAsJson hardcodes status: "ready_for_handoff" and omits projectName from the output. Other formatters include project name when available. If the JSON format is meant for programmatic consumption, including all available fields would be more useful.

Comment on lines +86 to +88
const filename = `decision-${decision.title
.toLowerCase()
.replace(/\s+/g, "-")}.${format === "markdown" ? "md" : format}`;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Decision title in filename is not sanitized — potential header injection or broken downloads.

decision.title is user-controlled content used directly in the Content-Disposition header. Titles with characters like ", \n, or non-ASCII could break the header or produce invalid filenames.

Proposed fix
-    const filename = `decision-${decision.title
-      .toLowerCase()
-      .replace(/\s+/g, "-")}.${format === "markdown" ? "md" : format}`;
+    const safeTitle = decision.title
+      .toLowerCase()
+      .replace(/[^a-z0-9]+/g, "-")
+      .replace(/^-|-$/g, "")
+      .slice(0, 100);
+    const ext = format === "markdown" ? "md" : format === "json" ? "json" : "txt";
+    const filename = `decision-${safeTitle}.${ext}`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const filename = `decision-${decision.title
.toLowerCase()
.replace(/\s+/g, "-")}.${format === "markdown" ? "md" : format}`;
const safeTitle = decision.title
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "")
.slice(0, 100);
const ext = format === "markdown" ? "md" : format === "json" ? "json" : "txt";
const filename = `decision-${safeTitle}.${ext}`;
🤖 Prompt for AI Agents
In `@app/api/projects/`[id]/decisions/[decisionId]/export/route.ts around lines 86
- 88, The filename creation uses raw user input (decision.title) and must be
sanitized before putting it in the Content-Disposition header; update the code
that builds filename (the filename const in route.ts) to (1) normalize and trim
the title, (2) remove or replace control characters like newlines and quotes and
any path separators, (3) restrict to a safe subset (e.g., convert to ASCII-like
slug with only letters, numbers, hyphens, and underscores, collapsing
consecutive non-safe chars), and (4) provide a safe fallback (e.g., "decision")
if the result is empty; finally ensure the header uses an encoded filename
fallback (RFC5987 / encodeURIComponent) when setting Content-Disposition so
non-ASCII characters are handled safely.

decision.status === "handed_off" ? "default" : "secondary"
}
>
{decision.status.replace("_", " ")}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

replace("_", " ") only replaces the first underscore.

String.prototype.replace with a string pattern replaces only the first occurrence. If statuses like "in_progress_review" exist, this would produce "in progress_review".

Proposed fix
-              {decision.status.replace("_", " ")}
+              {decision.status.replaceAll("_", " ")}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{decision.status.replace("_", " ")}
{decision.status.replaceAll("_", " ")}
🤖 Prompt for AI Agents
In `@app/projects/`[id]/decisions/[decisionId]/page.tsx at line 62, The code uses
decision.status.replace("_", " ") which only replaces the first underscore;
update the rendering logic that references decision.status.replace to replace
all underscores (e.g., use a global replacement or split/join) so
multi-underscore statuses like "in_progress_review" become "in progress review".
Ensure this change is applied where decision.status.replace is used in the
page.tsx render.

Comment on lines +54 to +57
<Button variant="outline" size="sm">
<Filter className="w-4 h-4 mr-2" />
Filter
</Button>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Filter button is non-functional.

This button renders but has no onClick handler or linked behavior. Consider either wiring it up or removing it to avoid confusing users.

🤖 Prompt for AI Agents
In `@app/projects/`[id]/decisions/page.tsx around lines 54 - 57, The Filter button
(the Button element rendering the Filter icon in page.tsx) is non-functional;
add an onClick handler to either toggle a local filter UI state (e.g., a
useState like showFilters and a handler such as handleToggleFilters) or call the
existing filter-opening function if one exists, and ensure the Button's onClick
wires to that handler (or remove the Button entirely if filtering is not
implemented) so the control is not misleading to users.

Comment on lines +363 to +364
{trigger ? (
<div onClick={() => setIsOpen(true)}>{trigger}</div>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Custom trigger path doesn't initialize preview and has accessibility issues.

Two problems:

  1. Empty preview on open: When opened via the custom trigger, updatePreview is never called, so the preview pane is blank. Compare with the default button path (line 370–371) which does call updatePreview.
  2. Accessibility: Wrapping trigger in a plain <div onClick> provides no keyboard support (Enter/Space), no role="button", and no tabIndex. Since the trigger on the list page is an actual <Button>, the outer <div> intercepts the click but doesn't add keyboard semantics.
Proposed fix
-        <div onClick={() => setIsOpen(true)}>{trigger}</div>
+        <div
+          role="button"
+          tabIndex={0}
+          onClick={() => {
+            setIsOpen(true);
+            updatePreview(selectedFormat);
+          }}
+          onKeyDown={(e) => {
+            if (e.key === "Enter" || e.key === " ") {
+              e.preventDefault();
+              setIsOpen(true);
+              updatePreview(selectedFormat);
+            }
+          }}
+        >
+          {trigger}
+        </div>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{trigger ? (
<div onClick={() => setIsOpen(true)}>{trigger}</div>
{trigger ? (
<div
role="button"
tabIndex={0}
onClick={() => {
setIsOpen(true);
updatePreview(selectedFormat);
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
setIsOpen(true);
updatePreview(selectedFormat);
}
}}
>
{trigger}
</div>
🤖 Prompt for AI Agents
In `@components/DecisionExportDialog.tsx` around lines 363 - 364, The custom
trigger wrapper currently only does setIsOpen(true) which both fails to call
updatePreview (so the preview is empty) and creates accessibility issues by
using a plain <div>; change the trigger handling to invoke updatePreview()
before opening (call updatePreview() then setIsOpen(true)) and make the trigger
keyboard-accessible by forwarding proper button semantics instead of a bare div
— e.g., use React.cloneElement(trigger, { onClick: () => { updatePreview();
setIsOpen(true); }, onKeyDown: (e) => { if (e.key === 'Enter' || e.key === ' ')
{ e.preventDefault(); updatePreview(); setIsOpen(true); } }, role: 'button',
tabIndex: 0 }) so the custom trigger receives click/keyboard handlers,
role="button" and tabIndex to match how the default Button path triggers
updatePreview and supports keyboard interaction.

Comment on lines +380 to +381
{isOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Modal lacks standard accessibility patterns.

The custom modal overlay has no Escape key handler, no focus trap, and no aria-* attributes (role="dialog", aria-modal="true", aria-labelledby). Consider using a proper dialog primitive (e.g., Radix Dialog which this project likely already has via shadcn/ui) instead of a hand-rolled modal.

#!/bin/bash
# Check if Radix Dialog is already available in the project
fd "dialog" --type f --extension tsx --search-path components/ui

Also applies to: 391-397

🤖 Prompt for AI Agents
In `@components/DecisionExportDialog.tsx` around lines 380 - 381, The custom modal
in DecisionExportDialog (the isOpen-controlled overlay div) lacks standard
accessibility: replace the hand-rolled overlay and content with the Radix/Dialog
(or the project's Dialog primitive from components/ui) so you get built-in focus
trap, Escape handling, and return-focus behavior; ensure the dialog root is
controlled by the existing isOpen state and calls the component's onClose/close
handler when closed, and add aria attributes (role="dialog" / aria-modal="true"
and aria-labelledby pointing to the dialog title element) for the dialog content
that previously lived inside the fixed inset div.

Comment on lines +456 to +462
<Button
size="sm"
onClick={() => handleExport(selectedFormat)}
>
<Download className="w-4 h-4 mr-1" />
Download
</Button>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

The "Download" button doesn't actually download a file.

handleExport only calls the optional onExport callback. Neither the detail page nor the list page passes onExport, so clicking "Download" is a no-op. Actual file-download logic (e.g., creating a Blob and triggering a link click) is missing.

Proposed fix — add download logic
   const handleExport = (format: ExportFormat) => {
     const content = updatePreview(format);
+    // Trigger file download
+    const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
+    const url = URL.createObjectURL(blob);
+    const a = document.createElement("a");
+    const ext = format === "markdown" ? "md" : format === "json" ? "json" : "txt";
+    a.href = url;
+    a.download = `decision-${decision.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 100)}.${ext}`;
+    document.body.appendChild(a);
+    a.click();
+    document.body.removeChild(a);
+    URL.revokeObjectURL(url);
     if (onExport) {
       onExport(format, content);
     }
   };
🤖 Prompt for AI Agents
In `@components/DecisionExportDialog.tsx` around lines 456 - 462, The Download
button currently calls handleExport(selectedFormat) which only invokes the
optional onExport callback and does nothing when onExport is not provided;
implement fallback download logic inside handleExport in DecisionExportDialog:
when onExport is undefined, generate the export payload (based on selectedFormat
and existing export data/helpers in the component), create a Blob, create an
object URL, programmatically create and click an anchor (<a>) with download
attribute, and revoke the URL after use so the file actually downloads; keep the
existing onExport call path intact and only run the Blob/link code as the
fallback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant