Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ describe('CreateDropActionsRow', () => {
const button = screen.getByRole('button', { name: /select audio file/i });
const input = button.querySelector('input') as HTMLInputElement;
fireEvent.change(input, { target: { files } });
expect(toast).toHaveBeenCalledWith({ message: 'You can only upload up to 4 files at a time', type: 'error' });
expect(toast).toHaveBeenCalledWith({
message: `You can only upload up to ${MAX_DROP_UPLOAD_FILES} files at a time`,
type: 'error'
});
expect(setFiles).not.toHaveBeenCalled();
});

Expand All @@ -38,6 +41,13 @@ describe('CreateDropActionsRow', () => {
expect(toast).not.toHaveBeenCalled();
});

it('accepts csv files in the input picker', () => {
renderComponent({ canAddPart: false, isStormMode: false, setFiles: jest.fn(), breakIntoStorm: jest.fn() });
const button = screen.getByRole('button', { name: /select audio file/i });
const input = button.querySelector('input') as HTMLInputElement;
expect(input).toHaveAttribute('accept', 'image/*,video/*,audio/*,.csv,text/csv');
});

it('renders break into storm button when allowed and handles click', () => {
const handler = jest.fn();
renderComponent({ canAddPart: true, isStormMode: false, setFiles: jest.fn(), breakIntoStorm: handler });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ jest.mock('@/components/drops/view/item/content/media/DropListItemContentMediaIm
jest.mock('@/components/drops/view/item/content/media/DropListItemContentMediaVideo', () => ({ __esModule: true, default: () => <div data-testid="video" /> }));
jest.mock('@/components/drops/view/item/content/media/DropListItemContentMediaAudio', () => ({ __esModule: true, default: () => <div data-testid="audio" /> }));
jest.mock('@/components/drops/view/item/content/media/DropListItemContentMediaGLB', () => ({ __esModule: true, default: () => <div data-testid="glb" /> }));
jest.mock('@/components/drops/view/item/content/media/DropMediaAttachmentCard', () => ({ __esModule: true, default: () => <div data-testid="attachment" /> }));
jest.mock('next/dynamic', () => (importer: any) => () => <div data-testid="glb" />);

describe('DropListItemContentMedia', () => {
Expand All @@ -29,6 +30,11 @@ describe('DropListItemContentMedia', () => {
expect(screen.getByTestId('glb')).toBeInTheDocument();
});

it('renders attachment card for csv', () => {
render(<DropListItemContentMedia media_mime_type="text/csv" media_url="file.csv" />);
expect(screen.getByTestId('attachment')).toBeInTheDocument();
});

it('renders empty fragment for unknown type', () => {
const { container } = render(<DropListItemContentMedia media_mime_type="text/plain" media_url="file.txt" />);
expect(container.firstChild).toBeNull();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ jest.mock('@/components/drops/view/item/content/media/MediaDisplayVideo', () =>
jest.mock('@/components/drops/view/item/content/media/MediaDisplayAudio', () => (props: any) => (
<div data-testid="audio" data-src={props.src} data-controls={String(props.showControls)} />
));
jest.mock('@/components/drops/view/item/content/media/DropMediaAttachmentCard', () => (props: any) => (
<div data-testid="attachment" data-src={props.src} data-mime={props.mimeType} />
));

jest.mock('next/dynamic', () => (importFn: any) => importFn().then ? () => <div data-testid="glb" /> : () => <div data-testid="glb" />);

Expand Down Expand Up @@ -41,6 +44,13 @@ describe('MediaDisplay', () => {
expect(screen.getByTestId('glb')).toBeInTheDocument();
});

it('renders attachment card for csv', () => {
render(<MediaDisplay media_mime_type="text/csv" media_url="file.csv" />);
const node = screen.getByTestId('attachment');
expect(node).toHaveAttribute('data-src', 'file.csv');
expect(node).toHaveAttribute('data-mime', 'text/csv');
});

it('renders empty fragment for unknown', () => {
const { container } = render(<MediaDisplay media_mime_type="text/plain" media_url="file.txt" />);
expect(container).toBeEmptyDOMElement();
Expand Down
19 changes: 19 additions & 0 deletions __tests__/components/waves/CreateDropActions.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,25 @@ describe("CreateDropActions", () => {
expect(fileInput).toHaveAttribute("multiple");
});

it("accepts csv uploads in chat mode only", () => {
const { rerender } = render(
<CreateDropActions {...defaultProps} isDropMode={false} />
);

let fileInputs = screen.getAllByLabelText("Upload a file");
let fileInput = fileInputs[0]?.querySelector('input[type="file"]');
expect(fileInput).toHaveAttribute(
"accept",
"image/*,video/*,audio/*,.csv,text/csv"
);

rerender(<CreateDropActions {...defaultProps} isDropMode={true} />);

fileInputs = screen.getAllByLabelText("Upload a file");
fileInput = fileInputs[0]?.querySelector('input[type="file"]');
expect(fileInput).toHaveAttribute("accept", "image/*,video/*,audio/*");
});

it("passes correct props to StormButton", () => {
render(
<CreateDropActions
Expand Down
19 changes: 19 additions & 0 deletions __tests__/services/uploads/multipartUploadCore.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { getContentType } from "@/services/uploads/multipartUploadCore";

describe("getContentType", () => {
it("normalizes csv files to text/csv", () => {
const file = new File(["a,b"], "data.csv", {
type: "application/vnd.ms-excel",
});

expect(getContentType(file)).toBe("text/csv");
});

it("keeps non-csv mime types unchanged", () => {
const file = new File(["x"], "image.png", {
type: "image/png",
});

expect(getContentType(file)).toBe("image/png");
});
});
2 changes: 1 addition & 1 deletion components/drops/create/utils/CreateDropActionsRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default function CreateDropActionsRow({
<input
type="file"
className="tw-hidden"
accept="image/*,video/*,audio/*"
accept="image/*,video/*,audio/*,.csv,text/csv"
Comment thread
GelatoGenesis marked this conversation as resolved.
Outdated
multiple
onChange={(e) => {
if (e.target.files) {
Expand Down
22 changes: 22 additions & 0 deletions components/drops/create/utils/file/CreateDropSelectedFileIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ enum FILE_TYPES {
IMAGE = "IMAGE",
VIDEO = "VIDEO",
AUDIO = "AUDIO",
CSV = "CSV",
}

export default function CreateDropSelectedFileIcon({
Expand All @@ -24,6 +25,9 @@ export default function CreateDropSelectedFileIcon({
if (file.type.includes("audio")) {
return FILE_TYPES.AUDIO;
}
if (file.type === "text/csv" || file.name.toLowerCase().endsWith(".csv")) {
return FILE_TYPES.CSV;
}
return null;
};

Expand Down Expand Up @@ -88,6 +92,24 @@ export default function CreateDropSelectedFileIcon({
></path>
</svg>
);
case FILE_TYPES.CSV:
return (
<svg
className="tw-flex-shrink-0 tw-h-5 tw-w-5 tw-text-iron-400"
xmlns="http://www.w3.org/2000/svg"
fill="none"
aria-hidden="true"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19.5 14.25v-8.25a2.25 2.25 0 0 0-2.25-2.25H8.25L4.5 7.5v10.5a2.25 2.25 0 0 0 2.25 2.25h6.75m0-9h6m-6 3h6m-6 3h3"
></path>
</svg>
);
default:
assertUnreachable(fileType);
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ enum FILE_TYPES {
IMAGE = "IMAGE",
VIDEO = "VIDEO",
AUDIO = "AUDIO",
CSV = "CSV",
UNKNOWN = "UNKNOWN",
}

Expand All @@ -21,6 +22,9 @@ export default function CreateDropSelectedFilePreview({
if (file.type.includes("audio")) {
return FILE_TYPES.AUDIO;
}
if (file.type === "text/csv" || file.name.toLowerCase().endsWith(".csv")) {
return FILE_TYPES.CSV;
}
return FILE_TYPES.UNKNOWN;
};

Expand All @@ -46,6 +50,18 @@ export default function CreateDropSelectedFilePreview({
Your browser does not support the audio tag.
</audio>
),
[FILE_TYPES.CSV]: (
<div className="tw-flex tw-h-full tw-min-h-32 tw-w-full tw-items-center tw-justify-center tw-rounded-xl tw-border tw-border-iron-700 tw-bg-iron-900/60 tw-p-6 tw-text-center">
<div>
<div className="tw-text-sm tw-font-semibold tw-text-iron-100">
CSV attachment
</div>
<div className="tw-mt-1 tw-break-all tw-text-xs tw-text-iron-400">
{file.name}
</div>
</div>
</div>
),
[FILE_TYPES.UNKNOWN]: <></>,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import DropListItemContentMediaImage from "./DropListItemContentMediaImage";
import DropListItemContentMediaVideo from "./DropListItemContentMediaVideo";
import SandboxedExternalIframe from "@/components/common/SandboxedExternalIframe";
import InteractiveIcon from "@/components/drops/media/InteractiveIcon";
import DropMediaAttachmentCard from "./DropMediaAttachmentCard";

enum MediaType {
IMAGE = "IMAGE",
VIDEO = "VIDEO",
AUDIO = "AUDIO",
GLB = "GLB",
HTML = "HTML",
CSV = "CSV",
UNKNOWN = "UNKNOWN",
}

Expand Down Expand Up @@ -73,6 +75,9 @@ export default function DropListItemContentMedia({
if (media_mime_type === "text/html") {
return MediaType.HTML;
}
if (media_mime_type === "text/csv") {
return MediaType.CSV;
}
return MediaType.UNKNOWN;
};

Expand Down Expand Up @@ -127,6 +132,13 @@ export default function DropListItemContentMedia({
className="tw-h-full tw-w-full"
/>
);
case MediaType.CSV:
return (
<DropMediaAttachmentCard
src={media_url}
mimeType={media_mime_type}
/>
);
case MediaType.UNKNOWN:
return <></>;
default:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"use client";

import Download from "@/components/download/Download";
import { getFileInfoFromUrl } from "@/helpers/file.helpers";

export default function DropMediaAttachmentCard({
src,
mimeType,
}: {
readonly src: string;
readonly mimeType: string;
}) {
const fileInfo = getFileInfoFromUrl(src);
const extension =
fileInfo?.extension ?? (mimeType === "text/csv" ? "csv" : "");
const name = fileInfo?.name ?? "attachment";
const displayName = extension ? `${name}.${extension}` : name;
const label = mimeType === "text/csv" ? "CSV attachment" : "Attachment";

return (
<div className="tw-flex tw-h-full tw-w-full tw-items-center tw-justify-between tw-gap-4 tw-rounded-2xl tw-border tw-border-iron-700 tw-bg-iron-900/70 tw-p-5">
<div className="tw-flex tw-min-w-0 tw-items-center tw-gap-4">
<div className="tw-flex tw-h-12 tw-w-12 tw-flex-shrink-0 tw-items-center tw-justify-center tw-rounded-2xl tw-bg-iron-800 tw-text-iron-200">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className="tw-h-6 tw-w-6"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19.5 14.25v-8.25a2.25 2.25 0 0 0-2.25-2.25H8.25L4.5 7.5v10.5a2.25 2.25 0 0 0 2.25 2.25h6.75m0-9h6m-6 3h6m-6 3h3"
/>
</svg>
</div>
<div className="tw-min-w-0">
<div className="tw-text-xs tw-font-medium tw-uppercase tw-tracking-[0.16em] tw-text-iron-400">
{label}
</div>
<div className="tw-mt-1 tw-truncate tw-text-sm tw-font-semibold tw-text-iron-100">
{displayName}
</div>
</div>
</div>
<Download
href={src}
name={name}
extension={extension}
variant="text"
alwaysShowText={true}
/>
</div>
);
}
12 changes: 12 additions & 0 deletions components/drops/view/item/content/media/MediaDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import dynamic from "next/dynamic";
import SandboxedExternalIframe from "@/components/common/SandboxedExternalIframe";
import { ImageScale } from "@/helpers/image.helpers";
import MediaDisplayAudio from "./MediaDisplayAudio";
import DropMediaAttachmentCard from "./DropMediaAttachmentCard";
import MediaDisplayImage from "./MediaDisplayImage";
import MediaDisplayVideo from "./MediaDisplayVideo";

Expand All @@ -13,6 +14,7 @@ enum MediaType {
AUDIO = "AUDIO",
GLB = "GLB",
HTML = "HTML",
CSV = "CSV",
UNKNOWN = "UNKNOWN",
}

Expand Down Expand Up @@ -63,6 +65,9 @@ export default function MediaDisplay({
if (media_mime_type === "text/html") {
return MediaType.HTML;
}
if (media_mime_type === "text/csv") {
return MediaType.CSV;
Comment on lines +69 to +70

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle parameterized CSV MIME types in classifier.

media_mime_type === "text/csv" misses common values like text/csv; charset=utf-8. In those cases (especially when media_url has no .csv), this falls through to UNKNOWN and the attachment card won’t render.

Proposed fix
-    if (media_mime_type === "text/csv" || hasCsvFileExtension(media_url)) {
+    if (
+      media_mime_type.toLowerCase().startsWith("text/csv") ||
+      hasCsvFileExtension(media_url)
+    ) {
       return MediaType.CSV;
     }
📝 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
if (media_mime_type === "text/csv" || hasCsvFileExtension(media_url)) {
return MediaType.CSV;
if (
media_mime_type.toLowerCase().startsWith("text/csv") ||
hasCsvFileExtension(media_url)
) {
return MediaType.CSV;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/drops/view/item/content/media/MediaDisplay.tsx` around lines 69 -
70, The exact string comparison in the MediaType detection logic for CSV files
is too strict and doesn't account for parameterized MIME types like "text/csv;
charset=utf-8". In the MediaDisplay.tsx file where the condition checks
`media_mime_type === "text/csv"`, replace the exact equality check with a check
that handles parameterized MIME types by verifying if the media_mime_type starts
with "text/csv" using startsWith() method, so that CSV files with charset
parameters or other MIME type parameters are correctly identified as CSV type
instead of falling through to UNKNOWN.

}
return MediaType.UNKNOWN;
};

Expand Down Expand Up @@ -105,6 +110,13 @@ export default function MediaDisplay({
className="tw-h-full tw-w-full"
/>
);
case MediaType.CSV:
return (
<DropMediaAttachmentCard
src={media_url}
mimeType={media_mime_type}
/>
);
case MediaType.UNKNOWN:
return <></>;
default:
Expand Down
6 changes: 5 additions & 1 deletion components/waves/CreateDropActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ const CreateDropActions: React.FC<CreateDropActionsProps> = memo(
e.target.value = "";
};

const fileInputAccept = isDropMode
? "image/*,video/*,audio/*"
: "image/*,video/*,audio/*,.csv,text/csv";

useEffect(() => {
if (!showGifPicker || !gifPickerEnabled) return;

Expand Down Expand Up @@ -167,7 +171,7 @@ const CreateDropActions: React.FC<CreateDropActionsProps> = memo(
<input
type="file"
className="tw-hidden"
accept="image/*,video/*,audio/*"
accept={fileInputAccept}
multiple
onChange={onFiles}
/>
Expand Down
22 changes: 21 additions & 1 deletion components/waves/CreateDropContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ const isMetadataValuePresent = (value: string | number | null): boolean => {
const hasMetadataContent = (metadata: CreateDropMetadataType[]): boolean =>
metadata.some((item) => isMetadataValuePresent(item.value));

const isCsvFile = (file: File): boolean =>
file.type === "text/csv" || file.name.toLowerCase().endsWith(".csv");

const hasSubmissionContent = ({
markdown,
files,
Expand Down Expand Up @@ -966,7 +969,24 @@ const CreateDropContent: React.FC<CreateDropContentProps> = ({
}, [activeDrop, isApp, focusMobileInput]);

const handleFileChange = (newFiles: File[]) => {
let updatedFiles = [...files, ...newFiles];
let acceptedFiles = [...newFiles];

if (isDropMode) {
const rejectedCsvFiles = acceptedFiles.filter(isCsvFile);
if (rejectedCsvFiles.length) {
setToast({
message: "CSV attachments are only supported on chat drops.",
type: "error",
});
acceptedFiles = acceptedFiles.filter((file) => !isCsvFile(file));
}
}

if (!acceptedFiles.length) {
return;
}

let updatedFiles = [...files, ...acceptedFiles];
let removedCount = 0;

if (updatedFiles.length > MAX_DROP_UPLOAD_FILES) {
Expand Down
Loading
Loading