;
+
+const DEFAULT_VALUES: BlockNameFormSchema = {
+ blockName: "",
+};
+
+export const BlockDocumentCreateDialog = ({
+ open,
+ onOpenChange,
+ blockTypeSlug,
+ onCreated,
+}: BlockDocumentCreateDialogProps) => {
+ return (
+
+ );
+};
+
+const BlockDocumentCreateDialogSkeleton = () => {
+ return (
+
+
+
+
+
+ );
+};
+
+type BlockDocumentCreateDialogContentProps = {
+ blockTypeSlug: string;
+ onCreated: (blockDocumentId: string) => void;
+ onOpenChange: (open: boolean) => void;
+};
+
+const BlockDocumentCreateDialogContent = ({
+ blockTypeSlug,
+ onCreated,
+ onOpenChange,
+}: BlockDocumentCreateDialogContentProps) => {
+ const { data: blockType } = useSuspenseQuery(
+ buildGetBlockTypeQuery(blockTypeSlug),
+ );
+
+ const { data: blockSchemas } = useSuspenseQuery(
+ buildListFilterBlockSchemasQuery({
+ block_schemas: {
+ operator: "and_",
+ block_type_id: { any_: [blockType.id] },
+ },
+ offset: 0,
+ limit: 1,
+ }),
+ );
+
+ const blockSchema = blockSchemas[0];
+
+ if (!blockSchema) {
+ return (
+
+ No schema found for this block type.
+
+ );
+ }
+
+ return (
+
+ );
+};
+
+type BlockDocumentCreateFormProps = {
+ blockTypeId: string;
+ blockSchemaId: string;
+ blockSchemaFields: PrefectSchemaObject;
+ onCreated: (blockDocumentId: string) => void;
+ onOpenChange: (open: boolean) => void;
+};
+
+const BlockDocumentCreateForm = ({
+ blockTypeId,
+ blockSchemaId,
+ blockSchemaFields,
+ onCreated,
+ onOpenChange,
+}: BlockDocumentCreateFormProps) => {
+ const { values, setValues, errors, validateForm } = useSchemaForm();
+ const { createBlockDocument, isPending } = useCreateBlockDocument();
+
+ const form = useForm({
+ resolver: zodResolver(BlockNameFormSchema),
+ defaultValues: DEFAULT_VALUES,
+ });
+
+ const onSave = async (zodFormValues: BlockNameFormSchema) => {
+ try {
+ await validateForm({ schema: values });
+ if (errors.length > 0) {
+ return;
+ }
+ createBlockDocument(
+ {
+ block_schema_id: blockSchemaId,
+ block_type_id: blockTypeId,
+ is_anonymous: false,
+ data: values,
+ name: zodFormValues.blockName,
+ },
+ {
+ onSuccess: (res) => {
+ toast.success("Block created successfully");
+ onOpenChange(false);
+ onCreated(res.id);
+ },
+ onError: (err) => {
+ const message = "Unknown error while creating block.";
+ toast.error(message);
+ console.error(message, err);
+ },
+ },
+ );
+ } catch (err) {
+ const message = "Unknown error while validating block data.";
+ toast.error(message);
+ console.error(message, err);
+ }
+ };
+
+ return (
+
+
+ );
+};
diff --git a/ui-v2/src/components/blocks/block-document-create-dialog/index.ts b/ui-v2/src/components/blocks/block-document-create-dialog/index.ts
new file mode 100644
index 000000000000..67e9ee7a2755
--- /dev/null
+++ b/ui-v2/src/components/blocks/block-document-create-dialog/index.ts
@@ -0,0 +1 @@
+export { BlockDocumentCreateDialog } from "./block-document-create-dialog";
diff --git a/ui-v2/src/components/schemas/schema-form-input-block-document.test.tsx b/ui-v2/src/components/schemas/schema-form-input-block-document.test.tsx
new file mode 100644
index 000000000000..ee5bd4a92129
--- /dev/null
+++ b/ui-v2/src/components/schemas/schema-form-input-block-document.test.tsx
@@ -0,0 +1,84 @@
+import { render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { buildApiUrl, createWrapper, server } from "@tests/utils";
+import { mockPointerEvents } from "@tests/utils/browser";
+import { HttpResponse, http } from "msw";
+import { beforeAll, describe, expect, it, vi } from "vitest";
+import { createFakeBlockDocument } from "@/mocks";
+import { SchemaFormInputBlockDocument } from "./schema-form-input-block-document";
+
+describe("SchemaFormInputBlockDocument", () => {
+ beforeAll(mockPointerEvents);
+
+ const mockBlockDocuments = [
+ createFakeBlockDocument({ id: "block-1", name: "my_block_0" }),
+ createFakeBlockDocument({ id: "block-2", name: "my_block_1" }),
+ ];
+
+ const setupMocks = () => {
+ server.use(
+ http.post(buildApiUrl("/block_documents/filter"), () => {
+ return HttpResponse.json(mockBlockDocuments);
+ }),
+ );
+ };
+
+ it("renders the combobox", async () => {
+ setupMocks();
+
+ render(
+ ,
+ { wrapper: createWrapper() },
+ );
+
+ await waitFor(() =>
+ expect(screen.getByLabelText(/select a block/i)).toBeVisible(),
+ );
+ });
+
+ it("calls onValueChange with $ref when a block is selected", async () => {
+ setupMocks();
+ const mockOnValueChange = vi.fn();
+ const user = userEvent.setup();
+
+ render(
+ ,
+ { wrapper: createWrapper() },
+ );
+
+ await waitFor(() =>
+ expect(screen.getByLabelText(/select a block/i)).toBeVisible(),
+ );
+
+ await user.click(screen.getByLabelText(/select a block/i));
+ await user.click(screen.getByRole("option", { name: "my_block_0" }));
+
+ expect(mockOnValueChange).toHaveBeenCalledWith({ $ref: "block-1" });
+ });
+
+ it("displays the selected block document name", async () => {
+ setupMocks();
+
+ render(
+ ,
+ { wrapper: createWrapper() },
+ );
+
+ await waitFor(() => expect(screen.getByText("my_block_0")).toBeVisible());
+ });
+});
diff --git a/ui-v2/src/components/schemas/schema-form-input-block-document.tsx b/ui-v2/src/components/schemas/schema-form-input-block-document.tsx
new file mode 100644
index 000000000000..6c11b1e092d9
--- /dev/null
+++ b/ui-v2/src/components/schemas/schema-form-input-block-document.tsx
@@ -0,0 +1,56 @@
+import { useState } from "react";
+import { BlockDocumentCombobox } from "@/components/blocks/block-document-combobox";
+import { BlockDocumentCreateDialog } from "@/components/blocks/block-document-create-dialog";
+
+type BlockDocumentReferenceValue =
+ | {
+ $ref: string;
+ }
+ | undefined;
+
+type SchemaFormInputBlockDocumentProps = {
+ value: BlockDocumentReferenceValue;
+ onValueChange: (value: BlockDocumentReferenceValue) => void;
+ blockTypeSlug: string;
+ id: string;
+};
+
+export function SchemaFormInputBlockDocument({
+ value,
+ onValueChange,
+ blockTypeSlug,
+ id,
+}: SchemaFormInputBlockDocumentProps) {
+ const [createDialogOpen, setCreateDialogOpen] = useState(false);
+
+ const selectedBlockDocumentId = value?.$ref;
+
+ const handleSelect = (blockDocumentId: string | undefined) => {
+ if (blockDocumentId) {
+ onValueChange({ $ref: blockDocumentId });
+ } else {
+ onValueChange(undefined);
+ }
+ };
+
+ const handleCreated = (blockDocumentId: string) => {
+ onValueChange({ $ref: blockDocumentId });
+ };
+
+ return (
+
+ setCreateDialogOpen(true)}
+ />
+
+
+ );
+}
diff --git a/ui-v2/src/components/schemas/schema-form-input.tsx b/ui-v2/src/components/schemas/schema-form-input.tsx
index 3ad0970c88a5..63d1f2b152e1 100644
--- a/ui-v2/src/components/schemas/schema-form-input.tsx
+++ b/ui-v2/src/components/schemas/schema-form-input.tsx
@@ -2,6 +2,7 @@ import type { SchemaObject } from "openapi-typescript";
import { SchemaFormInputAllOf } from "./schema-form-input-all-of";
import { SchemaFormInputAnyOf } from "./schema-form-input-any-of";
import { SchemaFormInputArray } from "./schema-form-input-array";
+import { SchemaFormInputBlockDocument } from "./schema-form-input-block-document";
import { SchemaFormInputBoolean } from "./schema-form-input-boolean";
import { SchemaFormInputInteger } from "./schema-form-input-integer";
import { SchemaFormInputNull } from "./schema-form-input-null";
@@ -74,7 +75,17 @@ export function SchemaFormInput({
}
if ("blockTypeSlug" in property) {
- throw new Error("not implemented");
+ const blockTypeSlug = property.blockTypeSlug;
+ if (typeof blockTypeSlug === "string") {
+ return (
+
+ );
+ }
}
if (isAnyOfObject(property)) {
diff --git a/ui-v2/src/components/schemas/stories/properties.stories.tsx b/ui-v2/src/components/schemas/stories/properties.stories.tsx
index c798af81feaf..935034680f55 100644
--- a/ui-v2/src/components/schemas/stories/properties.stories.tsx
+++ b/ui-v2/src/components/schemas/stories/properties.stories.tsx
@@ -1,7 +1,15 @@
import type { Meta, StoryObj } from "@storybook/react";
+import { buildApiUrl } from "@tests/utils/handlers";
+import { HttpResponse, http } from "msw";
import type { PrefectSchemaObject } from "@/components/schemas/types/schemas";
+import { createFakeBlockDocument } from "@/mocks";
+import { reactQueryDecorator } from "@/storybook/utils";
import { TestSchemaForm } from "./utilities";
+const MOCK_BLOCK_DOCUMENTS = Array.from({ length: 5 }, (_, i) =>
+ createFakeBlockDocument({ id: `block-${i}`, name: `my-block-${i}` }),
+);
+
const userDefinition: PrefectSchemaObject = {
type: "object",
title: "User",
@@ -23,6 +31,7 @@ const userDefinition: PrefectSchemaObject = {
const meta = {
title: "Components/SchemaForm/Properties",
component: TestSchemaForm,
+ decorators: [reactQueryDecorator],
parameters: {
layout: "fullscreen",
},
@@ -619,3 +628,58 @@ export const prefectKindWorkspaceVariableWithValue: Story = {
};
prefectKindWorkspaceVariableWithValue.storyName =
"prefect_kind:workspace_variable (with value)";
+
+export const blockTypeSlugEmpty: Story = {
+ args: {
+ schema: {
+ type: "object",
+ properties: {
+ credentials: {
+ title: "AWS Credentials",
+ // @ts-expect-error blockTypeSlug is a custom property not in the schema types
+ blockTypeSlug: "aws-credentials",
+ },
+ },
+ },
+ },
+ parameters: {
+ msw: {
+ handlers: [
+ http.post(buildApiUrl("/block_documents/filter"), () => {
+ return HttpResponse.json(MOCK_BLOCK_DOCUMENTS);
+ }),
+ ],
+ },
+ },
+};
+blockTypeSlugEmpty.storyName = "blockTypeSlug (empty)";
+
+export const blockTypeSlugWithValue: Story = {
+ args: {
+ schema: {
+ type: "object",
+ properties: {
+ credentials: {
+ title: "AWS Credentials",
+ // @ts-expect-error blockTypeSlug is a custom property not in the schema types
+ blockTypeSlug: "aws-credentials",
+ },
+ },
+ },
+ values: {
+ credentials: {
+ $ref: "block-0",
+ },
+ },
+ },
+ parameters: {
+ msw: {
+ handlers: [
+ http.post(buildApiUrl("/block_documents/filter"), () => {
+ return HttpResponse.json(MOCK_BLOCK_DOCUMENTS);
+ }),
+ ],
+ },
+ },
+};
+blockTypeSlugWithValue.storyName = "blockTypeSlug (with value)";