Skip to content

Fix CSV preview duplicate key warning (#10920)#17754

Open
bugisthegod wants to merge 1 commit intotwentyhq:mainfrom
bugisthegod:fix/10920-csv-duplicate-key-warning
Open

Fix CSV preview duplicate key warning (#10920)#17754
bugisthegod wants to merge 1 commit intotwentyhq:mainfrom
bugisthegod:fix/10920-csv-duplicate-key-warning

Conversation

@bugisthegod
Copy link

Fix issue #10920
image

The root cause is that @cyntler/react-doc-viewer's CSV renderer uses cell values as React keys instead of indices.

There are two approaches:

  1. patch-package — Directly fix the key usage in the library's source code and pull a request to the author of @cyntler/react-doc-viewer to fix it.
image image
  1. Custom CSV renderer (My current code commit)

Changes

  • fetchCsvPreview.ts — Fetches CSV and parses it into headers and rows
  • DocumentViewer.tsx — Renders CSV with a custom table instead of passing it to DocViewer

Copilot AI review requested due to automatic review settings February 5, 2026 22:32
@github-actions
Copy link
Contributor

github-actions bot commented Feb 5, 2026

Welcome!

Hello there, congrats on your first PR! We're excited to have you contributing to this project.
By submitting your Pull Request, you acknowledge that you agree with the terms of our Contributor License Agreement.

Generated by 🚫 dangerJS against 54dfcc6

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 5, 2026

Greptile Overview

Greptile Summary

This PR replaces the DocViewer CSV rendering path with a custom CSV table to avoid React duplicate key warnings caused by @cyntler/react-doc-viewer using cell values as keys. fetchCsvPreview now fetches and parses the CSV into { headers, rows }, and DocumentViewer renders that data directly for .csv files while leaving other preview types unchanged.

Main issues to address before merge:

  • The CSV fetch effect has no cancellation/error handling, so failures can leave the UI stuck on “Loading csv ...” and rapid URL changes can race.
  • fetchCsvPreview currently parses non-OK responses and ignores parse errors, which can result in rendering arbitrary error pages as a CSV table instead of falling back to the existing “preview unavailable / download” UX.

Confidence Score: 3/5

  • This PR is close to safe to merge but needs basic failure handling for the new CSV fetch/parse path.
  • Core change (custom CSV renderer) is localized and addresses the duplicate-key warning, but the new async CSV pipeline does not handle non-OK responses, parse errors, or effect races/unmounts, which can lead to stuck loading or incorrect renders in common failure/fast-navigation scenarios.
  • packages/twenty-front/src/modules/activities/files/components/DocumentViewer.tsx; packages/twenty-front/src/modules/activities/files/utils/fetchCsvPreview.ts

Important Files Changed

Filename Overview
packages/twenty-front/src/modules/activities/files/components/DocumentViewer.tsx Adds custom CSV table rendering using fetchCsvPreview output; main risk is missing error handling/cancellation in the CSV useEffect, which can leave loading state stuck and cause races on URL changes.
packages/twenty-front/src/modules/activities/files/utils/fetchCsvPreview.ts Changes CSV preview from returning CSV text to returning {headers, rows} parsed with PapaParse; should treat non-OK responses and parse errors as failures instead of rendering arbitrary text.

Sequence Diagram

sequenceDiagram
  participant U as User
  participant DV as DocumentViewer
  participant F as fetchCsvPreview
  participant Net as fetch(documentUrl)
  participant P as Papa.parse

  U->>DV: Open file preview (documentUrl, documentName)
  alt fileExtension == csv
    DV->>F: fetchCsvPreview(documentUrl)
    F->>Net: GET documentUrl
    Net-->>F: Response
    F->>F: response.text()
    F->>P: parse(text, { preview: rows+1, header:false })
    P-->>F: { data, errors }
    F-->>DV: { headers, rows }
    DV-->>U: Render <table> headers/rows
  else other previewable
    DV-->>U: Render DocViewer
  end
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

2 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 197 to 201
useEffect(() => {
if (fileExtension === 'csv') {
fetchCsvPreview(documentUrl).then((content) => {
setCsvPreview(content);
});
fetchCsvPreview(documentUrl).then(setCsvPreview);
}
}, [documentUrl, fileExtension]);
Copy link
Contributor

Choose a reason for hiding this comment

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

CSV preview stuck loading

fetchCsvPreview(documentUrl).then(setCsvPreview) has no cancellation or error handling. If the fetch/parse rejects (network/CORS/non-2xx causing response.text() to throw, etc.), the component will stay on “Loading csv ...” and you’ll get an unhandled rejection. It can also race when documentUrl changes quickly, setting CSV state for the previous URL after the new one. Consider aborting/guarding the effect and handling errors by rendering the existing “Preview Not Available / Download” UI for CSV.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/twenty-front/src/modules/activities/files/components/DocumentViewer.tsx
Line: 197:201

Comment:
**CSV preview stuck loading**

`fetchCsvPreview(documentUrl).then(setCsvPreview)` has no cancellation or error handling. If the fetch/parse rejects (network/CORS/non-2xx causing `response.text()` to throw, etc.), the component will stay on “Loading csv ...” and you’ll get an unhandled rejection. It can also race when `documentUrl` changes quickly, setting CSV state for the previous URL after the new one. Consider aborting/guarding the effect and handling errors by rendering the existing “Preview Not Available / Download” UI for CSV.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +10 to 12
export const fetchCsvPreview = async (url: string): Promise<CsvPreviewData> => {
const response = await fetch(url);
const text = await response.text();
Copy link
Contributor

Choose a reason for hiding this comment

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

Non-OK responses parsed

fetchCsvPreview parses await response.text() without checking response.ok, so a 403/404 HTML body (or any non-CSV content) will still be parsed and rendered as a “CSV” table. PapaParse also exposes result.errors which is currently ignored, so malformed CSV can silently produce surprising headers/rows. This should be treated as a fetch/parse failure so the caller can show the normal “preview unavailable” path instead of rendering garbage.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/twenty-front/src/modules/activities/files/utils/fetchCsvPreview.ts
Line: 10:12

Comment:
**Non-OK responses parsed**

`fetchCsvPreview` parses `await response.text()` without checking `response.ok`, so a 403/404 HTML body (or any non-CSV content) will still be parsed and rendered as a “CSV” table. PapaParse also exposes `result.errors` which is currently ignored, so malformed CSV can silently produce surprising `headers/rows`. This should be treated as a fetch/parse failure so the caller can show the normal “preview unavailable” path instead of rendering garbage.

How can I resolve this? If you propose a fix, please make it concise.

fetchCsvPreview(documentUrl).then((content) => {
setCsvPreview(content);
});
fetchCsvPreview(documentUrl).then(setCsvPreview);
Copy link

Choose a reason for hiding this comment

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

Bug: The promise returned by fetchCsvPreview lacks a .catch() handler. A failed network request will cause the component to show a loading state indefinitely.
Severity: MEDIUM

Suggested Fix

Add a .catch() block to the promise chain after the call to fetchCsvPreview. Inside the handler, log the error and update the component's state to display an error message to the user instead of the loading indicator.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location:
packages/twenty-front/src/modules/activities/files/components/DocumentViewer.tsx#L199

Potential issue: The `useEffect` hook that calls `fetchCsvPreview` for CSV files does
not handle promise rejections. If the underlying `fetch` call fails due to a network
error, CORS issue, or an invalid URL, the promise will reject. Because there is no
`.catch()` block, the `csvPreview` state will never be updated, and the component will
remain indefinitely in a "Loading csv ..." state, with no error message or recovery path
for the user.

Did we get this right? 👍 / 👎 to inform future reviews.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes console warnings about duplicate React keys when previewing CSV files by implementing a custom CSV table renderer instead of relying on the @cyntler/react-doc-viewer library's flawed CSV renderer that used cell values as keys.

Changes:

  • Refactored fetchCsvPreview to return structured data (headers and rows arrays) instead of reformatted CSV text
  • Implemented custom CSV table component in DocumentViewer with proper React keys using indices
  • Removed dependency on DocViewer library for CSV rendering

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 8 comments.

File Description
packages/twenty-front/src/modules/activities/files/utils/fetchCsvPreview.ts Changed return type from string to structured CsvPreviewData object with headers and rows arrays; modified CSV parsing to not use header mode
packages/twenty-front/src/modules/activities/files/components/DocumentViewer.tsx Added custom StyledCsvTable component and rendering logic for CSV files; replaced DocViewer blob URL approach with direct table rendering

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +10 to 23
export const fetchCsvPreview = async (url: string): Promise<CsvPreviewData> => {
const response = await fetch(url);
const text = await response.text();

const result = Papa.parse(text, {
preview: DEFAULT_PREVIEW_ROWS,
const result = Papa.parse<string[]>(text, {
preview: DEFAULT_PREVIEW_ROWS + 1, // +1 for header row
skipEmptyLines: true,
header: true,
header: false,
});

const data = result.data as Record<string, string>[];

const csvContent = Papa.unparse(data, {
header: true,
});
const [headers = [], ...rows] = result.data;

return csvContent;
return { headers, rows };
};
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The fetchCsvPreview function lacks test coverage. Other utility functions in this directory (downloadFile, getFileType) have corresponding test files in tests/. Consider adding tests for this function to verify correct CSV parsing, header extraction, and edge cases like empty CSVs, CSVs with only headers, or malformed data.

Copilot uses AI. Check for mistakes.
};

export const fetchCsvPreview = async (url: string): Promise<CsvPreviewData> => {
const response = await fetch(url);
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The fetch call lacks error handling for network failures or non-2xx HTTP status codes. If the fetch fails or returns an error status, the function will throw an unhandled error. Consider adding error handling similar to other fetch calls in the codebase, which check response.ok and throw appropriate errors with status information.

Suggested change
const response = await fetch(url);
let response: Response;
try {
response = await fetch(url);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to fetch CSV preview from "${url}": ${message}`);
}
if (!response.ok) {
throw new Error(
`Failed to fetch CSV preview from "${url}": ${response.status} ${response.statusText}`,
);
}

Copilot uses AI. Check for mistakes.
Comment on lines +14 to 18
const result = Papa.parse<string[]>(text, {
preview: DEFAULT_PREVIEW_ROWS + 1, // +1 for header row
skipEmptyLines: true,
header: true,
header: false,
});
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

Papa.parse can encounter errors during CSV parsing (e.g., malformed CSV files). The result object has an 'errors' array that should be checked. Consider validating result.errors and handling parse errors gracefully to prevent displaying malformed data or providing unclear error messages to users.

Copilot uses AI. Check for mistakes.
const csvContent = Papa.unparse(data, {
header: true,
});
const [headers = [], ...rows] = result.data;
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

If the CSV file is empty or contains only a header row with no data rows, the destructuring will work but rows will be an empty array. While this is technically correct, consider whether an empty CSV should show an empty table or a specific message to users indicating there's no data to preview.

Copilot uses AI. Check for mistakes.
Comment on lines +85 to +95
const StyledCsvTable = styled.table`
width: 100%;
text-align: left;
border-collapse: collapse;
color: ${({ theme }) => theme.font.color.primary};
th,
td {
padding: 5px 10px;
}
`;
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The custom CSV table lacks overflow handling. The StyledDocumentViewerContainer has overflow:auto for other renderers (line 52), but StyledCsvTable doesn't inherit this behavior. For CSVs with many columns or wide content, the table may overflow the container without scrollbars. Consider wrapping the table in a scrollable container or adding overflow styles to handle wide tables.

Copilot uses AI. Check for mistakes.
Comment on lines 197 to 201
useEffect(() => {
if (fileExtension === 'csv') {
fetchCsvPreview(documentUrl).then((content) => {
setCsvPreview(content);
});
fetchCsvPreview(documentUrl).then(setCsvPreview);
}
}, [documentUrl, fileExtension]);
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The useEffect hook that calls fetchCsvPreview does not handle errors. If the fetch or parsing fails, the csvPreview state will remain undefined, and users will see the "Loading csv ..." message indefinitely. Consider adding error handling with try-catch and displaying an appropriate error message to users when CSV loading fails.

Copilot uses AI. Check for mistakes.
th,
td {
padding: 5px 10px;
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The CSV table cells render raw text without any sanitization or escaping. While React automatically escapes text content to prevent XSS, consider edge cases where cell content might contain very long strings without spaces, which could break the table layout. Adding word-break or overflow-wrap styles to table cells would prevent layout issues.

Suggested change
padding: 5px 10px;
padding: 5px 10px;
word-break: break-word;
overflow-wrap: anywhere;

Copilot uses AI. Check for mistakes.
<thead>
<tr>
{csvPreview.headers.map((header, columnIndex) => (
<th key={columnIndex}>{header}</th>
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The table headers and cells lack proper ARIA attributes or semantic attributes that could enhance accessibility. While the basic table structure is semantically correct, consider whether headers should have scope attributes (scope="col") to explicitly indicate they're column headers, especially for screen reader users navigating complex CSV data.

Suggested change
<th key={columnIndex}>{header}</th>
<th key={columnIndex} scope="col">
{header}
</th>

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 2 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/twenty-front/src/modules/activities/files/components/DocumentViewer.tsx">

<violation number="1" location="packages/twenty-front/src/modules/activities/files/components/DocumentViewer.tsx:199">
P2: CSV preview state isn’t reset on documentUrl change, so switching between CSVs can show stale table data until the new fetch resolves or fails.</violation>
</file>

Since this is your first cubic review, here's how it works:

  • cubic automatically reviews your code and comments on bugs and improvements
  • Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
  • Ask questions if you need clarification on any suggestion

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

fetchCsvPreview(documentUrl).then((content) => {
setCsvPreview(content);
});
fetchCsvPreview(documentUrl).then(setCsvPreview);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 5, 2026

Choose a reason for hiding this comment

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

P2: CSV preview state isn’t reset on documentUrl change, so switching between CSVs can show stale table data until the new fetch resolves or fails.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/twenty-front/src/modules/activities/files/components/DocumentViewer.tsx, line 199:

<comment>CSV preview state isn’t reset on documentUrl change, so switching between CSVs can show stale table data until the new fetch resolves or fails.</comment>

<file context>
@@ -175,15 +192,11 @@ export const DocumentViewer = ({
-      fetchCsvPreview(documentUrl).then((content) => {
-        setCsvPreview(content);
-      });
+      fetchCsvPreview(documentUrl).then(setCsvPreview);
     }
   }, [documentUrl, fileExtension]);
</file context>
Suggested change
fetchCsvPreview(documentUrl).then(setCsvPreview);
setCsvPreview(undefined);
fetchCsvPreview(documentUrl).then(setCsvPreview);
Fix with Cubic

@github-actions
Copy link
Contributor

github-actions bot commented Feb 5, 2026

🚀 Preview Environment Ready!

Your preview environment is available at: http://bore.pub:24705

This environment will automatically shut down when the PR is closed or after 5 hours.

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