Skip to content
Draft
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
4 changes: 3 additions & 1 deletion src/components/Project/AddDocumentDialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,9 @@ describe("AddDocumentDialog", () => {
await act(async () => options.onSuccess());

expect(notify).toHaveBeenCalledWith("Document added", "success");
expect(track).toHaveBeenCalledWith("projects.add_document_completed");
expect(track).toHaveBeenCalledWith("projects.add_document_completed", {
kind: "document",
});
await waitFor(() => {
expect(screen.queryByRole("dialog")).toBeNull();
});
Expand Down
24 changes: 19 additions & 5 deletions src/components/Project/AddDocumentDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,37 +8,50 @@ import {
} from "@/components/ui/dialog";
import useToastNotification from "@/hooks/useToastNotification";
import { useAnalytics } from "@/providers/AnalyticsProvider";
import type { DocumentKind } from "@/services/projects/resourceDescriptor";
import type { CreateResourceInput } from "@/services/projects/types";
import { useCreateProjectResource } from "@/services/projects/useProjectResources";

import { DocumentForm } from "./DocumentForm";

const TITLE: Record<DocumentKind, string> = {
document: "Add Document",
code: "Add Code",
};

const ADDED: Record<DocumentKind, string> = {
document: "Document added",
code: "Code added",
};

interface AddDocumentDialogProps {
projectId: string;
open: boolean;
onOpenChange: (open: boolean) => void;
kind?: DocumentKind;
}

export function AddDocumentDialog({
projectId,
open,
onOpenChange,
kind = "document",
}: AddDocumentDialogProps) {
const createResource = useCreateProjectResource(projectId);
const notify = useToastNotification();
const { track } = useAnalytics();

useEffect(() => {
if (open) {
track("projects.add_document_dialog_impression");
track("projects.add_document_dialog_impression", { kind });
}
}, [open, track]);
}, [open, track, kind]);

const handleSubmit = (input: CreateResourceInput) => {
createResource.mutate(input, {
onSuccess: () => {
track("projects.add_document_completed");
notify("Document added", "success");
track("projects.add_document_completed", { kind });
notify(ADDED[kind], "success");
onOpenChange(false);
},
});
Expand All @@ -48,12 +61,13 @@ export function AddDocumentDialog({
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-4xl">
<DialogHeader>
<DialogTitle>Add Document</DialogTitle>
<DialogTitle>{TITLE[kind]}</DialogTitle>
</DialogHeader>
{/* Keyed on `open` so a dialog reopened after a cancel starts empty:
the fields live in the form, which the dialog keeps mounted. */}
<DocumentForm
key={String(open)}
kind={kind}
onSubmit={handleSubmit}
onCancel={() => onOpenChange(false)}
isSubmitting={createResource.isPending}
Expand Down
10 changes: 10 additions & 0 deletions src/components/Project/AddResourceMenu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ describe("AddResourceMenu", () => {
expect(await screen.findByRole("dialog")).toHaveTextContent("Add Document");
});

it("opens the same form for code, under its own name", async () => {
const user = await openMenu();

await user.click(screen.getByRole("menuitem", { name: "Code" }));

const dialog = await screen.findByRole("dialog");
expect(dialog).toHaveTextContent("Add Code");
expect(screen.getByLabelText("Filename")).toBeInTheDocument();
});

it("opens the pipeline picker", async () => {
await openMenu();

Expand Down
21 changes: 19 additions & 2 deletions src/components/Project/AddResourceMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Icon } from "@/components/ui/icon";
import type { DocumentKind } from "@/services/projects/resourceDescriptor";
import { resourceMeta } from "@/services/projects/resourceMeta";
import type { ProjectResourceSummary } from "@/services/projects/types";
import { tracking } from "@/utils/tracking";

Expand All @@ -25,8 +27,14 @@ export function AddResourceMenu({
resources,
}: AddResourceMenuProps) {
const [documentOpen, setDocumentOpen] = useState(false);
const [documentKind, setDocumentKind] = useState<DocumentKind>("document");
const [pipelineOpen, setPipelineOpen] = useState(false);

const openDocument = (kind: DocumentKind) => {
setDocumentKind(kind);
setDocumentOpen(true);
};

return (
<>
<DropdownMenu>
Expand All @@ -43,13 +51,21 @@ export function AddResourceMenu({
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem
onSelect={() => setDocumentOpen(true)}
{...tracking("projects.add_document_open")}
onSelect={() => openDocument("document")}
{...tracking("projects.add_document_open", { kind: "document" })}
>
<Icon name={entityIcon("document")} size="sm" />
Document
</DropdownMenuItem>

<DropdownMenuItem
onSelect={() => openDocument("code")}
{...tracking("projects.add_document_open", { kind: "code" })}
>
<Icon name={resourceMeta("code").icon} size="sm" />
Code
</DropdownMenuItem>

<DropdownMenuItem
onSelect={() => setPipelineOpen(true)}
{...tracking("projects.add_pipeline_open")}
Expand All @@ -64,6 +80,7 @@ export function AddResourceMenu({
projectId={projectId}
open={documentOpen}
onOpenChange={setDocumentOpen}
kind={documentKind}
/>

{/* Mounted only while open so the project page does not read browser
Expand Down
16 changes: 16 additions & 0 deletions src/components/Project/DocumentEditor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,22 @@ describe("DocumentEditor", () => {
expect(screen.queryByRole("alert")).toBeNull();
});

it("reports saving code under the same event as a document, told apart by kind", () => {
renderEditor(
resource({
metadata: { type: "code" },
payload: { content: "print(1)" },
}),
);

const save = screen.getByRole("button", { name: "Save" });
expect(save).toHaveAttribute("data-tracking-id", "projects.save_document");
expect(save).toHaveAttribute(
"data-tracking-metadata",
JSON.stringify({ kind: "code" }),
);
});

it("opens an empty document ready to be written into", async () => {
renderEditor(resource({ name: null, payload: {} }));

Expand Down
8 changes: 5 additions & 3 deletions src/components/Project/DocumentEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Icon } from "@/components/ui/icon";
import { BlockStack, InlineStack } from "@/components/ui/layout";
import { Text } from "@/components/ui/typography";
import { UNTITLED } from "@/services/projects/placeholderNames";
import { documentKindOf } from "@/services/projects/resourceDescriptor";
import { resourceMeta } from "@/services/projects/resourceMeta";
import type { ProjectResource } from "@/services/projects/types";
import { useUpdateProjectResource } from "@/services/projects/useProjectResources";
Expand Down Expand Up @@ -42,6 +43,7 @@ export function DocumentEditor({ projectId, resource }: DocumentEditorProps) {
const changedElsewhere = edit !== null && edit.startedFrom !== saved;
const canSave = isDirty && !update.isPending;
const name = resource.name ?? UNTITLED;
const kind = documentKindOf(resource);

const handleChange = (value: string) => {
setEdit((current) => ({
Expand Down Expand Up @@ -84,7 +86,7 @@ export function DocumentEditor({ projectId, resource }: DocumentEditorProps) {
className="w-full shrink-0 border-b border-border px-3 py-1.5"
>
<Icon
name={resourceMeta("document").icon}
name={resourceMeta(kind).icon}
size="xs"
className="shrink-0 text-muted-foreground"
/>
Expand All @@ -102,15 +104,15 @@ export function DocumentEditor({ projectId, resource }: DocumentEditorProps) {
size="xs"
disabled={edit === null || update.isPending}
onClick={discard}
{...tracking("projects.discard_document_edits")}
{...tracking("projects.discard_document_edits", { kind })}
>
Discard
</Button>
<Button
size="xs"
disabled={!canSave}
onClick={save}
{...tracking("projects.save_document")}
{...tracking("projects.save_document", { kind })}
>
{update.isPending ? "Saving…" : "Save"}
</Button>
Expand Down
79 changes: 75 additions & 4 deletions src/components/Project/DocumentForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";

import type { DocumentKind } from "@/services/projects/resourceDescriptor";

import { DocumentForm } from "./DocumentForm";

const track = vi.fn();
Expand Down Expand Up @@ -43,12 +45,21 @@ const form = () => titleField().closest("form") as HTMLFormElement;
const draggingHint = () =>
screen.queryByText("Drop to fill in the title and content");

function renderForm({ isSubmitting = false } = {}) {
function renderForm({
isSubmitting = false,
kind,
onSubmit = vi.fn(),
}: {
isSubmitting?: boolean;
kind?: DocumentKind;
onSubmit?: (input: unknown) => void;
} = {}) {
render(
<DocumentForm
onSubmit={vi.fn()}
onSubmit={onSubmit}
onCancel={vi.fn()}
isSubmitting={isSubmitting}
kind={kind}
/>,
);
}
Expand Down Expand Up @@ -88,7 +99,7 @@ describe("DocumentForm file drop", () => {
expect(titleField()).toHaveValue("model-card.md");
expect(track).toHaveBeenCalledWith(
"projects.add_document_from_file_completed",
{ method: "drop" },
{ method: "drop", kind: "document" },
);
});

Expand All @@ -114,7 +125,7 @@ describe("DocumentForm file drop", () => {
expect(titleField()).toHaveValue("model-card.md");
expect(track).toHaveBeenCalledWith(
"projects.add_document_from_file_completed",
{ method: "browse" },
{ method: "browse", kind: "document" },
);
});

Expand Down Expand Up @@ -240,3 +251,63 @@ describe("DocumentForm file drop", () => {
expect(draggingHint()).toBeNull();
});
});

describe("DocumentForm kinds", () => {
afterEach(() => {
vi.resetAllMocks();
});

it("adds a document by default, filed under its title", async () => {
const onSubmit = vi.fn();
const user = userEvent.setup();
renderForm({ onSubmit });

await user.type(titleField(), "Model card");
await user.type(contentField(), "Trained on Q3");
await user.click(addButton());

expect(onSubmit).toHaveBeenCalledWith(
expect.objectContaining({
name: "Model card",
metadata: { type: "document" },
}),
);
});

it("adds code by its filename, filed as code", async () => {
const onSubmit = vi.fn();
const user = userEvent.setup();
renderForm({ kind: "code", onSubmit });

const filename = screen.getByLabelText("Filename");
expect(filename).toHaveAttribute("placeholder", "train.py");
expect(screen.queryByLabelText("Title")).toBeNull();

await user.type(filename, "train.py");
await user.type(contentField(), "print(1)");
expect(contentField()).toHaveAttribute("data-language", "python");

await user.click(addButton());

expect(onSubmit).toHaveBeenCalledWith({
entity: "document",
name: "train.py",
payload: { content: "print(1)" },
metadata: { type: "code" },
});
});

it("says which kind a dropped file became", async () => {
renderForm({ kind: "code" });

fireEvent.drop(contentField(), fileDrag(modelCard()));

await waitFor(() => {
expect(contentField()).toHaveValue("Trained on Q3 data");
});
expect(track).toHaveBeenCalledWith(
"projects.add_document_from_file_completed",
{ method: "drop", kind: "code" },
);
});
});
Loading
Loading