Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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('does not expose csv in the legacy 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/*');
});

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");
});
});
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;
}
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