-
Notifications
You must be signed in to change notification settings - Fork 0
Add block document reference support to schema form #30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: coderabbit_full_base_add_block_document_reference_support_to_schema_form_pr4
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| import type { Meta, StoryObj } from "@storybook/react"; | ||
| import { buildApiUrl } from "@tests/utils/handlers"; | ||
| import { HttpResponse, http } from "msw"; | ||
| import { useState } from "react"; | ||
| import { createFakeBlockDocument } from "@/mocks"; | ||
| import { reactQueryDecorator } from "@/storybook/utils"; | ||
| import { BlockDocumentCombobox } from "./block-document-combobox"; | ||
|
|
||
| const MOCK_BLOCK_DOCUMENTS_DATA = Array.from({ length: 5 }, (_, i) => | ||
| createFakeBlockDocument({ name: `my-block-${i}` }), | ||
| ); | ||
|
|
||
| const meta = { | ||
| title: "Components/Blocks/BlockDocumentCombobox", | ||
| render: (args) => <BlockDocumentComboboxStory {...args} />, | ||
| decorators: [reactQueryDecorator], | ||
| parameters: { | ||
| msw: { | ||
| handlers: [ | ||
| http.post(buildApiUrl("/block_documents/filter"), () => { | ||
| return HttpResponse.json(MOCK_BLOCK_DOCUMENTS_DATA); | ||
| }), | ||
| ], | ||
| }, | ||
| }, | ||
| args: { | ||
| blockTypeSlug: "aws-credentials", | ||
| }, | ||
| } satisfies Meta<{ blockTypeSlug: string; showCreateNew?: boolean }>; | ||
|
|
||
| export default meta; | ||
|
|
||
| type Story = StoryObj<typeof meta>; | ||
|
|
||
| export const Default: Story = { name: "BlockDocumentCombobox" }; | ||
|
|
||
| export const WithCreateNew: Story = { | ||
| name: "With Create New Button", | ||
| args: { | ||
| showCreateNew: true, | ||
| }, | ||
| }; | ||
|
|
||
| export const Empty: Story = { | ||
| name: "Empty State", | ||
| parameters: { | ||
| msw: { | ||
| handlers: [ | ||
| http.post(buildApiUrl("/block_documents/filter"), () => { | ||
| return HttpResponse.json([]); | ||
| }), | ||
| ], | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| const BlockDocumentComboboxStory = ({ | ||
| blockTypeSlug, | ||
| showCreateNew, | ||
| }: { | ||
| blockTypeSlug: string; | ||
| showCreateNew?: boolean; | ||
| }) => { | ||
| const [selected, setSelected] = useState<string | undefined>(); | ||
|
|
||
| return ( | ||
| <BlockDocumentCombobox | ||
| blockTypeSlug={blockTypeSlug} | ||
| selectedBlockDocumentId={selected} | ||
| onSelect={setSelected} | ||
| onCreateNew={ | ||
| showCreateNew ? () => alert("Create new clicked") : undefined | ||
| } | ||
| /> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| 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 type { components } from "@/api/prefect"; | ||
| import { createFakeBlockDocument } from "@/mocks"; | ||
| import { BlockDocumentCombobox } from "./block-document-combobox"; | ||
|
|
||
| describe("BlockDocumentCombobox", () => { | ||
| beforeAll(mockPointerEvents); | ||
|
|
||
| const mockListBlockDocumentsAPI = ( | ||
| blockDocuments: Array<components["schemas"]["BlockDocument"]>, | ||
| ) => { | ||
| server.use( | ||
| http.post(buildApiUrl("/block_documents/filter"), () => { | ||
| return HttpResponse.json(blockDocuments); | ||
| }), | ||
| ); | ||
| }; | ||
|
|
||
| it("able to select a block document", async () => { | ||
| const mockOnSelect = vi.fn(); | ||
| const blockDocuments = [ | ||
| createFakeBlockDocument({ id: "block-1", name: "my_block_0" }), | ||
| createFakeBlockDocument({ id: "block-2", name: "my_block_1" }), | ||
| ]; | ||
| mockListBlockDocumentsAPI(blockDocuments); | ||
|
|
||
| const user = userEvent.setup(); | ||
|
|
||
| render( | ||
| <BlockDocumentCombobox | ||
| blockTypeSlug="aws-credentials" | ||
| selectedBlockDocumentId={undefined} | ||
| onSelect={mockOnSelect} | ||
| />, | ||
| { 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(mockOnSelect).toHaveBeenLastCalledWith("block-1"); | ||
| }); | ||
|
|
||
| it("has the selected value displayed", async () => { | ||
| const blockDocuments = [ | ||
| createFakeBlockDocument({ id: "block-1", name: "my_block_0" }), | ||
| createFakeBlockDocument({ id: "block-2", name: "my_block_1" }), | ||
| ]; | ||
| mockListBlockDocumentsAPI(blockDocuments); | ||
|
|
||
| render( | ||
| <BlockDocumentCombobox | ||
| blockTypeSlug="aws-credentials" | ||
| selectedBlockDocumentId="block-1" | ||
| onSelect={vi.fn()} | ||
| />, | ||
| { wrapper: createWrapper() }, | ||
| ); | ||
|
|
||
| await waitFor(() => expect(screen.getByText("my_block_0")).toBeVisible()); | ||
| }); | ||
|
|
||
| it("shows placeholder when no block document is selected", async () => { | ||
| mockListBlockDocumentsAPI([]); | ||
|
|
||
| render( | ||
| <BlockDocumentCombobox | ||
| blockTypeSlug="aws-credentials" | ||
| selectedBlockDocumentId={undefined} | ||
| onSelect={vi.fn()} | ||
| />, | ||
| { wrapper: createWrapper() }, | ||
| ); | ||
|
|
||
| await waitFor(() => | ||
| expect(screen.getByText("Select a block...")).toBeVisible(), | ||
| ); | ||
| }); | ||
|
|
||
| it("shows create new button when onCreateNew is provided", async () => { | ||
| const mockOnCreateNew = vi.fn(); | ||
| mockListBlockDocumentsAPI([]); | ||
|
|
||
| const user = userEvent.setup(); | ||
|
|
||
| render( | ||
| <BlockDocumentCombobox | ||
| blockTypeSlug="aws-credentials" | ||
| selectedBlockDocumentId={undefined} | ||
| onSelect={vi.fn()} | ||
| onCreateNew={mockOnCreateNew} | ||
| />, | ||
| { 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: /create new block/i })); | ||
|
|
||
| expect(mockOnCreateNew).toHaveBeenCalled(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,129 @@ | ||||||||||||||||||||||||||||||||||||||||||||||
| import { useSuspenseQuery } from "@tanstack/react-query"; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { Suspense, useDeferredValue, useMemo, useState } from "react"; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { buildListFilterBlockDocumentsQuery } from "@/api/block-documents"; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||
| Combobox, | ||||||||||||||||||||||||||||||||||||||||||||||
| ComboboxCommandEmtpy, | ||||||||||||||||||||||||||||||||||||||||||||||
| ComboboxCommandGroup, | ||||||||||||||||||||||||||||||||||||||||||||||
| ComboboxCommandInput, | ||||||||||||||||||||||||||||||||||||||||||||||
| ComboboxCommandItem, | ||||||||||||||||||||||||||||||||||||||||||||||
| ComboboxCommandList, | ||||||||||||||||||||||||||||||||||||||||||||||
| ComboboxContent, | ||||||||||||||||||||||||||||||||||||||||||||||
| ComboboxTrigger, | ||||||||||||||||||||||||||||||||||||||||||||||
| } from "@/components/ui/combobox"; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { Icon } from "@/components/ui/icons"; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| type BlockDocumentComboboxProps = { | ||||||||||||||||||||||||||||||||||||||||||||||
| blockTypeSlug: string; | ||||||||||||||||||||||||||||||||||||||||||||||
| selectedBlockDocumentId: string | undefined; | ||||||||||||||||||||||||||||||||||||||||||||||
| onSelect: (blockDocumentId: string | undefined) => void; | ||||||||||||||||||||||||||||||||||||||||||||||
| onCreateNew?: () => void; | ||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| export const BlockDocumentCombobox = ({ | ||||||||||||||||||||||||||||||||||||||||||||||
| blockTypeSlug, | ||||||||||||||||||||||||||||||||||||||||||||||
| selectedBlockDocumentId, | ||||||||||||||||||||||||||||||||||||||||||||||
| onSelect, | ||||||||||||||||||||||||||||||||||||||||||||||
| onCreateNew, | ||||||||||||||||||||||||||||||||||||||||||||||
| }: BlockDocumentComboboxProps) => { | ||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||
| <Suspense> | ||||||||||||||||||||||||||||||||||||||||||||||
| <BlockDocumentComboboxImplementation | ||||||||||||||||||||||||||||||||||||||||||||||
| blockTypeSlug={blockTypeSlug} | ||||||||||||||||||||||||||||||||||||||||||||||
| selectedBlockDocumentId={selectedBlockDocumentId} | ||||||||||||||||||||||||||||||||||||||||||||||
| onSelect={onSelect} | ||||||||||||||||||||||||||||||||||||||||||||||
| onCreateNew={onCreateNew} | ||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||
| </Suspense> | ||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const BlockDocumentComboboxImplementation = ({ | ||||||||||||||||||||||||||||||||||||||||||||||
| blockTypeSlug, | ||||||||||||||||||||||||||||||||||||||||||||||
| selectedBlockDocumentId, | ||||||||||||||||||||||||||||||||||||||||||||||
| onSelect, | ||||||||||||||||||||||||||||||||||||||||||||||
| onCreateNew, | ||||||||||||||||||||||||||||||||||||||||||||||
| }: BlockDocumentComboboxProps) => { | ||||||||||||||||||||||||||||||||||||||||||||||
| const [search, setSearch] = useState(""); | ||||||||||||||||||||||||||||||||||||||||||||||
| const deferredSearch = useDeferredValue(search); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const { data } = useSuspenseQuery( | ||||||||||||||||||||||||||||||||||||||||||||||
| buildListFilterBlockDocumentsQuery({ | ||||||||||||||||||||||||||||||||||||||||||||||
| offset: 0, | ||||||||||||||||||||||||||||||||||||||||||||||
| sort: "BLOCK_TYPE_AND_NAME_ASC", | ||||||||||||||||||||||||||||||||||||||||||||||
| include_secrets: false, | ||||||||||||||||||||||||||||||||||||||||||||||
| block_types: { | ||||||||||||||||||||||||||||||||||||||||||||||
| slug: { any_: [blockTypeSlug] }, | ||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||
| block_documents: { | ||||||||||||||||||||||||||||||||||||||||||||||
| operator: "and_", | ||||||||||||||||||||||||||||||||||||||||||||||
| is_anonymous: { eq_: false }, | ||||||||||||||||||||||||||||||||||||||||||||||
| ...(deferredSearch ? { name: { like_: deferredSearch } } : {}), | ||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||
| limit: 50, | ||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const filteredData = useMemo(() => { | ||||||||||||||||||||||||||||||||||||||||||||||
| return data.filter((blockDocument) => | ||||||||||||||||||||||||||||||||||||||||||||||
| blockDocument.name?.toLowerCase().includes(deferredSearch.toLowerCase()), | ||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||
| }, [data, deferredSearch]); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const selectedBlockDocument = useMemo(() => { | ||||||||||||||||||||||||||||||||||||||||||||||
| return filteredData.find( | ||||||||||||||||||||||||||||||||||||||||||||||
| (blockDocument) => blockDocument.id === selectedBlockDocumentId, | ||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||
| }, [filteredData, selectedBlockDocumentId]); | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+67
to
+77
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Client-side filtering may be redundant and Two concerns:
🐛 Suggested fix for the selected document lookup-const selectedBlockDocument = useMemo(() => {
- return filteredData.find(
- (blockDocument) => blockDocument.id === selectedBlockDocumentId,
- );
-}, [filteredData, selectedBlockDocumentId]);
+const selectedBlockDocument = useMemo(() => {
+ return data.find(
+ (blockDocument) => blockDocument.id === selectedBlockDocumentId,
+ );
+}, [data, selectedBlockDocumentId]);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||
| <Combobox> | ||||||||||||||||||||||||||||||||||||||||||||||
| <ComboboxTrigger | ||||||||||||||||||||||||||||||||||||||||||||||
| selected={Boolean(selectedBlockDocumentId)} | ||||||||||||||||||||||||||||||||||||||||||||||
| aria-label="Select a block" | ||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||
| {selectedBlockDocument?.name ?? "Select a block..."} | ||||||||||||||||||||||||||||||||||||||||||||||
| </ComboboxTrigger> | ||||||||||||||||||||||||||||||||||||||||||||||
| <ComboboxContent> | ||||||||||||||||||||||||||||||||||||||||||||||
| <ComboboxCommandInput | ||||||||||||||||||||||||||||||||||||||||||||||
| value={search} | ||||||||||||||||||||||||||||||||||||||||||||||
| onValueChange={setSearch} | ||||||||||||||||||||||||||||||||||||||||||||||
| placeholder="Search for a block..." | ||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||
| <ComboboxCommandEmtpy>No block found</ComboboxCommandEmtpy> | ||||||||||||||||||||||||||||||||||||||||||||||
| <ComboboxCommandList> | ||||||||||||||||||||||||||||||||||||||||||||||
| <ComboboxCommandGroup> | ||||||||||||||||||||||||||||||||||||||||||||||
| {filteredData.map((blockDocument) => ( | ||||||||||||||||||||||||||||||||||||||||||||||
| <ComboboxCommandItem | ||||||||||||||||||||||||||||||||||||||||||||||
| key={blockDocument.id} | ||||||||||||||||||||||||||||||||||||||||||||||
| selected={selectedBlockDocumentId === blockDocument.id} | ||||||||||||||||||||||||||||||||||||||||||||||
| onSelect={(value) => { | ||||||||||||||||||||||||||||||||||||||||||||||
| onSelect(value); | ||||||||||||||||||||||||||||||||||||||||||||||
| setSearch(""); | ||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||
| value={blockDocument.id} | ||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||
| {blockDocument.name} | ||||||||||||||||||||||||||||||||||||||||||||||
| </ComboboxCommandItem> | ||||||||||||||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||||||||||||||
| </ComboboxCommandGroup> | ||||||||||||||||||||||||||||||||||||||||||||||
| {onCreateNew && ( | ||||||||||||||||||||||||||||||||||||||||||||||
| <ComboboxCommandGroup> | ||||||||||||||||||||||||||||||||||||||||||||||
| <ComboboxCommandItem | ||||||||||||||||||||||||||||||||||||||||||||||
| onSelect={() => { | ||||||||||||||||||||||||||||||||||||||||||||||
| onCreateNew(); | ||||||||||||||||||||||||||||||||||||||||||||||
| setSearch(""); | ||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||
| value="__create_new__" | ||||||||||||||||||||||||||||||||||||||||||||||
| closeOnSelect={true} | ||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||
| <Icon id="Plus" className="mr-2 size-4" /> | ||||||||||||||||||||||||||||||||||||||||||||||
| Create new block | ||||||||||||||||||||||||||||||||||||||||||||||
| </ComboboxCommandItem> | ||||||||||||||||||||||||||||||||||||||||||||||
| </ComboboxCommandGroup> | ||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||
| </ComboboxCommandList> | ||||||||||||||||||||||||||||||||||||||||||||||
| </ComboboxContent> | ||||||||||||||||||||||||||||||||||||||||||||||
| </Combobox> | ||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { BlockDocumentCombobox } from "./block-document-combobox"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| import type { Meta, StoryObj } from "@storybook/react"; | ||
| import { buildApiUrl } from "@tests/utils/handlers"; | ||
| import { HttpResponse, http } from "msw"; | ||
| import { useState } from "react"; | ||
| import { Button } from "@/components/ui/button"; | ||
| import { createFakeBlockSchema, createFakeBlockType } from "@/mocks"; | ||
| import { reactQueryDecorator, toastDecorator } from "@/storybook/utils"; | ||
| import { BlockDocumentCreateDialog } from "./block-document-create-dialog"; | ||
|
|
||
| const MOCK_BLOCK_TYPE = createFakeBlockType({ | ||
| id: "block-type-1", | ||
| slug: "secret", | ||
| name: "Secret", | ||
| }); | ||
|
|
||
| const MOCK_BLOCK_SCHEMA = createFakeBlockSchema(); | ||
|
|
||
| const meta = { | ||
| title: "Components/Blocks/BlockDocumentCreateDialog", | ||
| render: (args) => <BlockDocumentCreateDialogStory {...args} />, | ||
| decorators: [reactQueryDecorator, toastDecorator], | ||
| parameters: { | ||
| msw: { | ||
| handlers: [ | ||
| http.get(buildApiUrl("/block_types/slug/:slug"), () => { | ||
| return HttpResponse.json(MOCK_BLOCK_TYPE); | ||
| }), | ||
| http.post(buildApiUrl("/block_schemas/filter"), () => { | ||
| return HttpResponse.json([ | ||
| { ...MOCK_BLOCK_SCHEMA, block_type_id: MOCK_BLOCK_TYPE.id }, | ||
| ]); | ||
| }), | ||
| http.post(buildApiUrl("/block_documents/"), () => { | ||
| return HttpResponse.json({ | ||
| id: "new-block-document-id", | ||
| name: "test-block", | ||
| }); | ||
| }), | ||
| ], | ||
| }, | ||
| }, | ||
| args: { | ||
| blockTypeSlug: "secret", | ||
| }, | ||
| } satisfies Meta<{ blockTypeSlug: string }>; | ||
|
|
||
| export default meta; | ||
|
|
||
| type Story = StoryObj<typeof meta>; | ||
|
|
||
| export const Default: Story = { name: "BlockDocumentCreateDialog" }; | ||
|
|
||
| const BlockDocumentCreateDialogStory = ({ | ||
| blockTypeSlug, | ||
| }: { | ||
| blockTypeSlug: string; | ||
| }) => { | ||
| const [open, setOpen] = useState(false); | ||
|
|
||
| return ( | ||
| <> | ||
| <Button onClick={() => setOpen(true)}>Open Dialog</Button> | ||
| <BlockDocumentCreateDialog | ||
| open={open} | ||
| onOpenChange={setOpen} | ||
| blockTypeSlug={blockTypeSlug} | ||
| onCreated={(id) => alert(`Created block document: ${id}`)} | ||
| /> | ||
| </> | ||
| ); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing Suspense fallback leaves users with no loading indication.
The
<Suspense>component has nofallbackprop, so users won't see any visual feedback while the block documents are loading. Consider adding a skeleton or loading indicator.🛡️ Suggested fix
🤖 Prompt for AI Agents