fix(ui): scope prompt row click and delete to the row's environment

This commit is contained in:
Cursor Agent 2026-08-27 04:42:43 +00:00
parent 78e7262722
commit ee6533b727
No known key found for this signature in database
7 changed files with 47 additions and 27 deletions

View file

@ -65,11 +65,11 @@ describe("PromptTable", () => {
expect(within(rows[1]).getByText("prompt-older")).toBeInTheDocument();
});
it("should call onPromptClick when the prompt ID is clicked", async () => {
it("should call onPromptClick with the row's environment when the prompt ID is clicked", async () => {
const user = userEvent.setup();
render(<PromptTable {...defaultProps} />);
await user.click(screen.getByRole("button", { name: "prompt-newer" }));
expect(mockOnPromptClick).toHaveBeenCalledWith("prompt-newer");
expect(mockOnPromptClick).toHaveBeenCalledWith("prompt-newer", "production");
});
it("should label the environment and default missing environments to development", () => {
@ -83,7 +83,7 @@ describe("PromptTable", () => {
render(<PromptTable {...defaultProps} />);
await user.click(screen.getByTestId("prompt-actions-prompt-newer"));
await user.click(await screen.findByTestId("prompt-action-delete"));
expect(mockOnDeleteClick).toHaveBeenCalledWith("prompt-newer", "prompt-newer");
expect(mockOnDeleteClick).toHaveBeenCalledWith("prompt-newer", "prompt-newer", "production");
});
it("should copy the prompt ID through the actions menu", async () => {

View file

@ -13,8 +13,8 @@ import { ModelGroupInfo } from "./prompt_utils";
interface PromptTableProps {
promptsList: PromptSpec[];
isLoading: boolean;
onPromptClick?: (id: string) => void;
onDeleteClick?: (id: string, name: string) => void;
onPromptClick?: (id: string, environment?: string) => void;
onDeleteClick?: (id: string, name: string, environment?: string) => void;
accessToken: string | null;
isAdmin: boolean;
}

View file

@ -64,7 +64,7 @@ function PromptModelCell({ prompt, modelHubData }: { prompt: PromptSpec; modelHu
interface PromptRowActionsProps {
prompt: PromptSpec;
isAdmin: boolean;
onDeleteClick?: (id: string, name: string) => void;
onDeleteClick?: (id: string, name: string, environment?: string) => void;
}
function PromptRowActions({ prompt, isAdmin, onDeleteClick }: PromptRowActionsProps) {
@ -91,7 +91,9 @@ function PromptRowActions({ prompt, isAdmin, onDeleteClick }: PromptRowActionsPr
<DropdownMenuItem
variant="destructive"
data-testid="prompt-action-delete"
onClick={() => onDeleteClick?.(prompt.prompt_id, prompt.prompt_id || "Unknown Prompt")}
onClick={() =>
onDeleteClick?.(prompt.prompt_id, prompt.prompt_id || "Unknown Prompt", prompt.environment)
}
>
<Trash2 />
Delete
@ -106,8 +108,8 @@ function PromptRowActions({ prompt, isAdmin, onDeleteClick }: PromptRowActionsPr
interface PromptTableColumnsDeps {
modelHubData: Map<string, ModelGroupInfo>;
isAdmin: boolean;
onPromptClick?: (id: string) => void;
onDeleteClick?: (id: string, name: string) => void;
onPromptClick?: (id: string, environment?: string) => void;
onDeleteClick?: (id: string, name: string, environment?: string) => void;
}
export const getPromptTableColumns = ({
@ -128,7 +130,7 @@ export const getPromptTableColumns = ({
title={row.original.prompt_id}
titleClassName="font-mono text-xs font-normal"
className="max-w-60"
onClick={onPromptClick ? () => onPromptClick(row.original.prompt_id) : undefined}
onClick={onPromptClick ? () => onPromptClick(row.original.prompt_id, row.original.environment) : undefined}
/>
),
},

View file

@ -18,11 +18,11 @@ vi.mock("./PromptTable", () => ({
onDeleteClick,
}: {
isLoading: boolean;
onDeleteClick: (id: string, name: string) => void;
onDeleteClick: (id: string, name: string, environment?: string) => void;
}) => (
<div data-testid="prompt-table">
{isLoading ? "table-loading" : "table-loaded"}
<button type="button" onClick={() => onDeleteClick("prompt-1", "my-prompt")}>
<button type="button" onClick={() => onDeleteClick("prompt-1", "my-prompt", "staging")}>
row-delete
</button>
</div>
@ -162,7 +162,7 @@ describe("PromptsPanel delete confirmation", () => {
await user.click(screen.getByRole("button", { name: /^delete$/i }));
await waitFor(() => expect(mockDeletePromptCall).toHaveBeenCalledWith("sk-test", "prompt-1"));
await waitFor(() => expect(mockDeletePromptCall).toHaveBeenCalledWith("sk-test", "prompt-1", "staging"));
});
it("should abandon the delete when the confirmation is dismissed", async () => {
@ -191,7 +191,7 @@ describe("PromptsPanel delete confirmation", () => {
await user.click(await screen.findByRole("button", { name: "row-delete" }));
await screen.findByText(/delete prompt: my-prompt/i);
await user.click(screen.getByRole("button", { name: /^delete$/i }));
await waitFor(() => expect(mockDeletePromptCall).toHaveBeenCalledWith("sk-test", "prompt-1"));
await waitFor(() => expect(mockDeletePromptCall).toHaveBeenCalledWith("sk-test", "prompt-1", "staging"));
await user.keyboard("{Escape}");
expect(screen.getByText(/delete prompt: my-prompt/i)).toBeInTheDocument();

View file

@ -41,11 +41,12 @@ const PromptsPanel: React.FC<PromptsProps> = ({ accessToken, userRole }) => {
const [isLoading, setIsLoading] = useState(true);
const [selectedEnvironment, setSelectedEnvironment] = useState<string | undefined>(undefined);
const [selectedPromptId, setSelectedPromptId] = useState<string | null>(null);
const [selectedPromptEnvironment, setSelectedPromptEnvironment] = useState<string | undefined>(undefined);
const [isAddModalVisible, setIsAddModalVisible] = useState(false);
const [showEditorView, setShowEditorView] = useState(false);
const [editPromptData, setEditPromptData] = useState<any>(null);
const [isDeleting, setIsDeleting] = useState(false);
const [promptToDelete, setPromptToDelete] = useState<{ id: string; name: string } | null>(null);
const [promptToDelete, setPromptToDelete] = useState<{ id: string; name: string; environment?: string } | null>(null);
// Admin Viewer follows the read-parity rule: see prompts, no writes.
const canModify = userRole ? isProxyAdminRole(userRole) : false;
@ -71,8 +72,9 @@ const PromptsPanel: React.FC<PromptsProps> = ({ accessToken, userRole }) => {
fetchPrompts();
}, [accessToken, selectedEnvironment]);
const handlePromptClick = (promptId: string) => {
const handlePromptClick = (promptId: string, environment?: string) => {
setSelectedPromptId(promptId);
setSelectedPromptEnvironment(environment);
};
const handleAddPrompt = () => {
@ -111,8 +113,8 @@ const PromptsPanel: React.FC<PromptsProps> = ({ accessToken, userRole }) => {
setSelectedPromptId(null);
};
const handleDeleteClick = (promptId: string, promptName: string) => {
setPromptToDelete({ id: promptId, name: promptName });
const handleDeleteClick = (promptId: string, promptName: string, environment?: string) => {
setPromptToDelete({ id: promptId, name: promptName, environment });
};
const handleDeleteConfirm = async () => {
@ -120,7 +122,7 @@ const PromptsPanel: React.FC<PromptsProps> = ({ accessToken, userRole }) => {
setIsDeleting(true);
try {
await deletePromptCall(accessToken, promptToDelete.id);
await deletePromptCall(accessToken, promptToDelete.id, promptToDelete.environment);
toast.success(`Prompt "${promptToDelete.name}" deleted successfully`);
fetchPrompts(); // Refresh the list
} catch (error) {
@ -148,7 +150,11 @@ const PromptsPanel: React.FC<PromptsProps> = ({ accessToken, userRole }) => {
) : selectedPromptId ? (
<PromptInfoView
promptId={selectedPromptId}
onClose={() => setSelectedPromptId(null)}
environment={selectedPromptEnvironment}
onClose={() => {
setSelectedPromptId(null);
setSelectedPromptEnvironment(undefined);
}}
accessToken={accessToken}
isAdmin={canModify}
onDelete={fetchPrompts}

View file

@ -20,6 +20,7 @@ import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "
export interface PromptInfoProps {
promptId: string;
environment?: string;
onClose: () => void;
accessToken: string | null;
isAdmin: boolean;
@ -27,7 +28,15 @@ export interface PromptInfoProps {
onEdit?: (promptData: any) => void;
}
const PromptInfoView: React.FC<PromptInfoProps> = ({ promptId, onClose, accessToken, isAdmin, onDelete, onEdit }) => {
const PromptInfoView: React.FC<PromptInfoProps> = ({
promptId,
environment,
onClose,
accessToken,
isAdmin,
onDelete,
onEdit,
}) => {
const [promptData, setPromptData] = useState<PromptSpec | null>(null);
const [promptTemplate, setPromptTemplate] = useState<PromptTemplateBase | null>(null);
const [rawApiResponse, setRawApiResponse] = useState<any>(null);
@ -86,11 +95,11 @@ const PromptInfoView: React.FC<PromptInfoProps> = ({ promptId, onClose, accessTo
const isInitialMount = useRef(true);
useEffect(() => {
setSelectedEnv(null);
setSelectedEnv(environment ?? null);
setEnvironments([]);
setVersionHistory([]);
fetchPromptInfo();
}, [promptId, accessToken]);
fetchPromptInfo(environment);
}, [promptId, accessToken, environment]);
// When environment changes (user clicks tab), re-fetch — skip initial mount
useEffect(() => {
@ -140,7 +149,7 @@ const PromptInfoView: React.FC<PromptInfoProps> = ({ promptId, onClose, accessTo
if (!accessToken || !promptData) return;
setIsDeleting(true);
try {
await deletePromptCall(accessToken, basePromptId);
await deletePromptCall(accessToken, basePromptId, selectedEnv ?? undefined);
toast.success(`Prompt "${basePromptId}" deleted successfully`);
onDelete?.();
onClose();

View file

@ -4625,9 +4625,12 @@ export const updatePromptCall = async (accessToken: string, promptId: string, pr
}
};
export const deletePromptCall = async (accessToken: string, promptId: string) => {
export const deletePromptCall = async (accessToken: string, promptId: string, environment?: string) => {
try {
const data = await apiClient.delete(`/prompts/${promptId}`, { accessToken });
const data = await apiClient.delete(`/prompts/${promptId}`, {
accessToken,
query: { environment: environment || undefined },
});
return data;
} catch (error) {
console.error("Failed to delete prompt:", error);