Redesign Extensions page. Remove enable toggle in UI. Treat Extension Manager as core MCP enabler per session. (#8940)
Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
This commit is contained in:
@@ -125,6 +125,8 @@ describe("acpNotificationHandler", () => {
|
||||
type: "toolRequest",
|
||||
id: "tool-1",
|
||||
name: "mcp_app_bench__inspect_host_info",
|
||||
toolName: "mcp_app_bench__inspect_host_info",
|
||||
extensionName: "mcp_app_bench",
|
||||
status: "completed",
|
||||
});
|
||||
expect(message.content[1]).toMatchObject({
|
||||
@@ -234,6 +236,11 @@ describe("acpNotificationHandler", () => {
|
||||
"mcpApp",
|
||||
"text",
|
||||
]);
|
||||
expect(buffer?.[1]?.content[0]).toMatchObject({
|
||||
type: "toolRequest",
|
||||
toolName: "mcp_app_bench__inspect_host_info",
|
||||
extensionName: "mcp_app_bench",
|
||||
});
|
||||
expect(buffer?.[1]?.content[2]).toMatchObject({
|
||||
type: "mcpApp",
|
||||
id: "tool-1",
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
clearReplayBuffer,
|
||||
getReplayBuffer,
|
||||
} from "@/features/chat/hooks/replayBuffer";
|
||||
import { useChatStore } from "@/features/chat/stores/chatStore";
|
||||
import {
|
||||
clearMessageTracking,
|
||||
handleSessionNotification,
|
||||
setActiveMessageId,
|
||||
} from "../acpNotificationHandler";
|
||||
import { registerSession } from "../acpSessionTracker";
|
||||
|
||||
describe("ACP tool call status handling", () => {
|
||||
beforeEach(() => {
|
||||
clearMessageTracking();
|
||||
clearReplayBuffer("replay-failed-tool-session");
|
||||
clearReplayBuffer("goose-session");
|
||||
useChatStore.setState({
|
||||
messagesBySession: {},
|
||||
sessionStateById: {},
|
||||
queuedMessageBySession: {},
|
||||
draftsBySession: {},
|
||||
activeSessionId: null,
|
||||
isConnected: false,
|
||||
loadingSessionIds: new Set<string>(),
|
||||
scrollTargetMessageBySession: {},
|
||||
});
|
||||
});
|
||||
|
||||
it("marks failed replay tool updates as errors", async () => {
|
||||
const replaySessionId = "replay-failed-tool-session";
|
||||
useChatStore.setState({
|
||||
loadingSessionIds: new Set<string>([replaySessionId]),
|
||||
});
|
||||
|
||||
await handleSessionNotification({
|
||||
sessionId: replaySessionId,
|
||||
update: {
|
||||
sessionUpdate: "tool_call",
|
||||
toolCallId: "tool-1",
|
||||
title: "shell",
|
||||
},
|
||||
} as never);
|
||||
|
||||
await handleSessionNotification({
|
||||
sessionId: replaySessionId,
|
||||
update: {
|
||||
sessionUpdate: "tool_call_update",
|
||||
toolCallId: "tool-1",
|
||||
status: "failed",
|
||||
content: [
|
||||
{
|
||||
type: "content",
|
||||
content: {
|
||||
type: "text",
|
||||
text: "Command failed.",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} as never);
|
||||
|
||||
const assistant = getReplayBuffer(replaySessionId)?.[0];
|
||||
expect(assistant?.content[0]).toMatchObject({
|
||||
type: "toolRequest",
|
||||
id: "tool-1",
|
||||
status: "error",
|
||||
});
|
||||
expect(assistant?.content[1]).toMatchObject({
|
||||
type: "toolResponse",
|
||||
id: "tool-1",
|
||||
isError: true,
|
||||
result: "Command failed.",
|
||||
});
|
||||
});
|
||||
|
||||
it("marks failed live tool updates as errors", async () => {
|
||||
registerSession(
|
||||
"local-session",
|
||||
"goose-session",
|
||||
"goose",
|
||||
"/Users/aharvard/.goose/artifacts",
|
||||
);
|
||||
setActiveMessageId("goose-session", "assistant-1");
|
||||
|
||||
await handleSessionNotification({
|
||||
sessionId: "goose-session",
|
||||
update: {
|
||||
sessionUpdate: "tool_call",
|
||||
toolCallId: "tool-1",
|
||||
title: "shell",
|
||||
},
|
||||
} as never);
|
||||
|
||||
await handleSessionNotification({
|
||||
sessionId: "goose-session",
|
||||
update: {
|
||||
sessionUpdate: "tool_call_update",
|
||||
toolCallId: "tool-1",
|
||||
status: "failed",
|
||||
content: [
|
||||
{
|
||||
type: "content",
|
||||
content: {
|
||||
type: "text",
|
||||
text: "Command failed.",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} as never);
|
||||
|
||||
const [message] =
|
||||
useChatStore.getState().messagesBySession["local-session"];
|
||||
expect(message.content[0]).toMatchObject({
|
||||
type: "toolRequest",
|
||||
id: "tool-1",
|
||||
status: "error",
|
||||
});
|
||||
expect(message.content[1]).toMatchObject({
|
||||
type: "toolResponse",
|
||||
id: "tool-1",
|
||||
isError: true,
|
||||
result: "Command failed.",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
findLatestUnpairedToolRequest,
|
||||
} from "@/features/chat/hooks/replayBuffer";
|
||||
import type {
|
||||
ToolCallStatus,
|
||||
ToolRequestContent,
|
||||
ToolResponseContent,
|
||||
} from "@/shared/types/messages";
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
getLocalSessionId,
|
||||
subscribeToSessionRegistration,
|
||||
} from "./acpSessionTracker";
|
||||
import { getToolCallIdentity } from "./acpToolCallIdentity";
|
||||
import { perfLog } from "@/shared/lib/perfLog";
|
||||
|
||||
// Pre-set message ID for the next live stream per goose session
|
||||
@@ -53,6 +55,9 @@ const pendingUsageUpdates = new Map<
|
||||
{ accumulatedTotal: number; contextLimit: number }
|
||||
>();
|
||||
|
||||
const toolCallStatusFromUpdate = (status: string): ToolCallStatus =>
|
||||
status === "failed" ? "error" : "completed";
|
||||
|
||||
subscribeToSessionRegistration((localSessionId, gooseSessionId) => {
|
||||
const pendingUsage = pendingUsageUpdates.get(gooseSessionId);
|
||||
if (!pendingUsage) {
|
||||
@@ -180,6 +185,7 @@ function handleReplay(
|
||||
|
||||
case "tool_call": {
|
||||
const created = getReplayCreated(update);
|
||||
const identity = getToolCallIdentity(update);
|
||||
const msg = ensureReplayAssistantMessage(
|
||||
sessionId,
|
||||
getReplayMessageId(update),
|
||||
@@ -189,6 +195,7 @@ function handleReplay(
|
||||
type: "toolRequest",
|
||||
id: update.toolCallId,
|
||||
name: update.title,
|
||||
...identity,
|
||||
arguments: {},
|
||||
status: "executing",
|
||||
startedAt: created ?? Date.now(),
|
||||
@@ -199,6 +206,7 @@ function handleReplay(
|
||||
case "tool_call_update": {
|
||||
const created = getReplayCreated(update);
|
||||
const replayMessageId = getReplayMessageId(update);
|
||||
const identity = getToolCallIdentity(update);
|
||||
const trackedMessageId = getTrackedReplayAssistantMessageId(sessionId);
|
||||
const replayMsg = replayMessageId
|
||||
? getBufferedMessage(sessionId, replayMessageId)
|
||||
@@ -216,15 +224,19 @@ function handleReplay(
|
||||
if (created !== undefined && !existingMsg && msg === replayMsg) {
|
||||
msg.created = created;
|
||||
}
|
||||
if (update.title) {
|
||||
if (update.title || Object.keys(identity).length > 0) {
|
||||
const tc = msg.content.find(
|
||||
(c) => c.type === "toolRequest" && c.id === update.toolCallId,
|
||||
);
|
||||
if (tc && tc.type === "toolRequest") {
|
||||
(tc as ToolRequestContent).name = update.title;
|
||||
Object.assign(tc as ToolRequestContent, {
|
||||
...(update.title ? { name: update.title } : {}),
|
||||
...identity,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (update.status === "completed" || update.status === "failed") {
|
||||
const toolCallStatus = toolCallStatusFromUpdate(update.status);
|
||||
const tc = msg.content.find(
|
||||
(c) => c.type === "toolRequest" && c.id === update.toolCallId,
|
||||
);
|
||||
@@ -233,7 +245,8 @@ function handleReplay(
|
||||
if (idx >= 0) {
|
||||
msg.content[idx] = {
|
||||
...tc,
|
||||
status: "completed",
|
||||
...identity,
|
||||
status: toolCallStatus,
|
||||
} as ToolRequestContent;
|
||||
}
|
||||
}
|
||||
@@ -299,11 +312,13 @@ function handleLive(
|
||||
|
||||
case "tool_call": {
|
||||
const messageId = ensureLiveAssistantMessage(sessionId, gooseSessionId);
|
||||
const identity = getToolCallIdentity(update);
|
||||
|
||||
const toolRequest: ToolRequestContent = {
|
||||
type: "toolRequest",
|
||||
id: update.toolCallId,
|
||||
name: update.title,
|
||||
...identity,
|
||||
arguments: {},
|
||||
status: "executing",
|
||||
startedAt: Date.now(),
|
||||
@@ -315,19 +330,25 @@ function handleLive(
|
||||
|
||||
case "tool_call_update": {
|
||||
const messageId = ensureLiveAssistantMessage(sessionId, gooseSessionId);
|
||||
const identity = getToolCallIdentity(update);
|
||||
|
||||
if (update.title) {
|
||||
if (update.title || Object.keys(identity).length > 0) {
|
||||
store.updateMessage(sessionId, messageId, (msg) => ({
|
||||
...msg,
|
||||
content: msg.content.map((c) =>
|
||||
c.type === "toolRequest" && c.id === update.toolCallId
|
||||
? { ...c, name: update.title ?? "" }
|
||||
? {
|
||||
...c,
|
||||
...(update.title ? { name: update.title } : {}),
|
||||
...identity,
|
||||
}
|
||||
: c,
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
if (update.status === "completed" || update.status === "failed") {
|
||||
const toolCallStatus = toolCallStatusFromUpdate(update.status);
|
||||
const streamingMessage = store.messagesBySession[sessionId]?.find(
|
||||
(m) => m.id === messageId,
|
||||
);
|
||||
@@ -339,7 +360,11 @@ function handleLive(
|
||||
...msg,
|
||||
content: msg.content.map((block) =>
|
||||
block.type === "toolRequest" && block.id === update.toolCallId
|
||||
? { ...block, status: "completed" }
|
||||
? {
|
||||
...block,
|
||||
...identity,
|
||||
status: toolCallStatus,
|
||||
}
|
||||
: block,
|
||||
),
|
||||
}));
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { SessionUpdate } from "@agentclientprotocol/sdk";
|
||||
|
||||
export interface ToolCallIdentity {
|
||||
toolName?: string;
|
||||
extensionName?: string;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
export function getToolCallIdentity(update: SessionUpdate): ToolCallIdentity {
|
||||
if (!isRecord(update._meta)) {
|
||||
return {};
|
||||
}
|
||||
const goose = update._meta.goose;
|
||||
if (!isRecord(goose)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const toolCall = isRecord(goose.mcpApp)
|
||||
? goose.mcpApp
|
||||
: isRecord(goose.toolCall)
|
||||
? goose.toolCall
|
||||
: null;
|
||||
if (!toolCall) return {};
|
||||
|
||||
return {
|
||||
...(typeof toolCall.toolName === "string"
|
||||
? { toolName: toolCall.toolName }
|
||||
: {}),
|
||||
...(typeof toolCall.extensionName === "string"
|
||||
? { extensionName: toolCall.extensionName }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
@@ -45,9 +45,7 @@
|
||||
"empty": {
|
||||
"folderNotSet": "Folder not set",
|
||||
"noChanges": "No uncommitted changes",
|
||||
"noProjectAssigned": "No project assigned",
|
||||
"noExtensions": "No extensions enabled",
|
||||
"noMatchingExtensions": "No matching extensions"
|
||||
"noProjectAssigned": "No project assigned"
|
||||
},
|
||||
"errors": {
|
||||
"gitRead": "Couldn't read git status."
|
||||
@@ -91,8 +89,6 @@
|
||||
"artifacts": "Artifacts",
|
||||
"changes": "Changes",
|
||||
"changesOnBranch": "on",
|
||||
"extensions": "Extensions",
|
||||
"searchExtensions": "Search extensions...",
|
||||
"workspace": "Workspace"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -83,16 +83,17 @@
|
||||
"extensions": {
|
||||
"title": "Extensions",
|
||||
"search": "Search extensions...",
|
||||
"enabled": "Enabled",
|
||||
"enabledCount": "Enabled ({{count}})",
|
||||
"available": "Available",
|
||||
"availableCount": "Available ({{count}})",
|
||||
"empty": "No extensions configured.",
|
||||
"noResults": "No extensions match your search.",
|
||||
"addExtension": "Add extension",
|
||||
"editExtension": "Edit Extension",
|
||||
"deleteExtension": "Delete Extension",
|
||||
"toggle": "Toggle {{name}}",
|
||||
"deleteConfirmation": {
|
||||
"title": "Delete \"{{name}}\" permanently?",
|
||||
"description": "This will permanently remove this extension and all of its settings.",
|
||||
"confirm": "Delete Extension",
|
||||
"deleting": "Deleting..."
|
||||
},
|
||||
"configure": "Configure {{name}}",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
@@ -120,8 +121,20 @@
|
||||
"streamable_http": "HTTP",
|
||||
"builtin": "Built-in"
|
||||
},
|
||||
"filters": {
|
||||
"all": "All"
|
||||
},
|
||||
"categories": {
|
||||
"appsServices": "Apps & services",
|
||||
"gooseCapabilities": "Goose capabilities"
|
||||
},
|
||||
"sections": {
|
||||
"extensions": "Extensions",
|
||||
"gooseCapabilities": "Built-in Goose capabilities"
|
||||
},
|
||||
"showGooseCapabilities": "Show {{count}} built-in Goose capabilities",
|
||||
"hideGooseCapabilities": "Hide built-in Goose capabilities",
|
||||
"errors": {
|
||||
"toggleFailed": "Failed to toggle extension. Please try again.",
|
||||
"saveFailed": "Failed to save extension. Please try again.",
|
||||
"deleteFailed": "Failed to delete extension. Please try again.",
|
||||
"nameConflict": "An extension named \"{{name}}\" already exists."
|
||||
@@ -198,7 +211,6 @@
|
||||
"doctor": "Doctor",
|
||||
"general": "General",
|
||||
"projects": "Projects",
|
||||
"extensions": "Extensions",
|
||||
"providers": "Providers",
|
||||
"voice": "Voice"
|
||||
},
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
},
|
||||
"navigation": {
|
||||
"agents": "Agents",
|
||||
"extensions": "Extensions",
|
||||
"home": "Home",
|
||||
"sessionHistory": "Session history",
|
||||
"skills": "Skills"
|
||||
|
||||
@@ -45,9 +45,7 @@
|
||||
"empty": {
|
||||
"folderNotSet": "Carpeta no configurada",
|
||||
"noChanges": "No hay cambios sin confirmar",
|
||||
"noProjectAssigned": "No hay proyecto asignado",
|
||||
"noExtensions": "No hay extensiones habilitadas",
|
||||
"noMatchingExtensions": "No hay extensiones que coincidan"
|
||||
"noProjectAssigned": "No hay proyecto asignado"
|
||||
},
|
||||
"errors": {
|
||||
"gitRead": "No se pudo leer el estado de git."
|
||||
@@ -91,8 +89,6 @@
|
||||
"artifacts": "Artefactos",
|
||||
"changes": "Cambios",
|
||||
"changesOnBranch": "en",
|
||||
"extensions": "Extensiones",
|
||||
"searchExtensions": "Buscar extensiones...",
|
||||
"workspace": "Espacio de trabajo"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -83,16 +83,17 @@
|
||||
"extensions": {
|
||||
"title": "Extensiones",
|
||||
"search": "Buscar extensiones...",
|
||||
"enabled": "Habilitadas",
|
||||
"enabledCount": "Habilitadas ({{count}})",
|
||||
"available": "Disponibles",
|
||||
"availableCount": "Disponibles ({{count}})",
|
||||
"empty": "No hay extensiones configuradas.",
|
||||
"noResults": "Ninguna extensión coincide con tu búsqueda.",
|
||||
"addExtension": "Agregar extensión",
|
||||
"editExtension": "Editar extensión",
|
||||
"deleteExtension": "Eliminar extensión",
|
||||
"toggle": "Activar/desactivar {{name}}",
|
||||
"deleteConfirmation": {
|
||||
"title": "¿Eliminar \"{{name}}\" de forma permanente?",
|
||||
"description": "Esto eliminará de forma permanente esta extensión y toda su configuración.",
|
||||
"confirm": "Eliminar extensión",
|
||||
"deleting": "Eliminando..."
|
||||
},
|
||||
"configure": "Configurar {{name}}",
|
||||
"save": "Guardar",
|
||||
"cancel": "Cancelar",
|
||||
@@ -120,8 +121,20 @@
|
||||
"streamable_http": "HTTP",
|
||||
"builtin": "Integrada"
|
||||
},
|
||||
"filters": {
|
||||
"all": "Todas"
|
||||
},
|
||||
"categories": {
|
||||
"appsServices": "Apps y servicios",
|
||||
"gooseCapabilities": "Capacidades de Goose"
|
||||
},
|
||||
"sections": {
|
||||
"extensions": "Extensiones",
|
||||
"gooseCapabilities": "Capacidades integradas de Goose"
|
||||
},
|
||||
"showGooseCapabilities": "Mostrar {{count}} capacidades integradas de Goose",
|
||||
"hideGooseCapabilities": "Ocultar capacidades integradas de Goose",
|
||||
"errors": {
|
||||
"toggleFailed": "Error al cambiar la extensión. Inténtalo de nuevo.",
|
||||
"saveFailed": "Error al guardar la extensión. Inténtalo de nuevo.",
|
||||
"deleteFailed": "Error al eliminar la extensión. Inténtalo de nuevo.",
|
||||
"nameConflict": "Ya existe una extensión llamada \"{{name}}\"."
|
||||
@@ -198,7 +211,6 @@
|
||||
"doctor": "Diagnóstico",
|
||||
"general": "General",
|
||||
"projects": "Proyectos",
|
||||
"extensions": "Extensiones",
|
||||
"providers": "Proveedores",
|
||||
"voice": "Voz"
|
||||
},
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
},
|
||||
"navigation": {
|
||||
"agents": "Agentes",
|
||||
"extensions": "Extensiones",
|
||||
"home": "Inicio",
|
||||
"sessionHistory": "Historial de sesiones",
|
||||
"skills": "Habilidades"
|
||||
|
||||
@@ -78,6 +78,8 @@ export interface ToolRequestContent {
|
||||
type: "toolRequest";
|
||||
id: string;
|
||||
name: string;
|
||||
toolName?: string;
|
||||
extensionName?: string;
|
||||
arguments: Record<string, unknown>;
|
||||
status: ToolCallStatus;
|
||||
/** Epoch ms when the tool call started executing (set on event receipt). */
|
||||
|
||||
@@ -21,7 +21,7 @@ export function SessionActivityIndicator({
|
||||
role="status"
|
||||
aria-label="Chat active"
|
||||
className={cn(
|
||||
"absolute -right-1 -top-1 flex h-3.5 w-3.5 items-center justify-center rounded-full border border-background bg-background shadow-sm transition-opacity duration-200 ease-out animate-in fade-in-0",
|
||||
"absolute -right-1 -top-1 flex h-3.5 w-3.5 items-center justify-center transition-opacity duration-200 ease-out animate-in fade-in-0",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { ConfirmDialog } from "./confirm-dialog";
|
||||
|
||||
describe("ConfirmDialog", () => {
|
||||
it("routes rejected confirm actions to onConfirmError", async () => {
|
||||
const user = userEvent.setup();
|
||||
const error = new Error("Delete failed");
|
||||
const onConfirm = vi.fn().mockRejectedValue(error);
|
||||
const onConfirmError = vi.fn();
|
||||
|
||||
render(
|
||||
<ConfirmDialog
|
||||
open
|
||||
onOpenChange={vi.fn()}
|
||||
title="Delete item?"
|
||||
description="This cannot be undone."
|
||||
cancelLabel="Cancel"
|
||||
confirmLabel="Delete"
|
||||
onConfirm={onConfirm}
|
||||
onConfirmError={onConfirmError}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Delete" }));
|
||||
|
||||
expect(onConfirm).toHaveBeenCalledOnce();
|
||||
await waitFor(() => {
|
||||
expect(onConfirmError).toHaveBeenCalledWith(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
import type * as React from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Button, type ButtonProps } from "@/shared/ui/button";
|
||||
|
||||
interface ConfirmDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
title: React.ReactNode;
|
||||
description: React.ReactNode;
|
||||
cancelLabel: React.ReactNode;
|
||||
confirmLabel: React.ReactNode;
|
||||
loadingLabel?: React.ReactNode;
|
||||
isLoading?: boolean;
|
||||
confirmVariant?: ButtonProps["variant"];
|
||||
contentClassName?: string;
|
||||
overlayClassName?: string;
|
||||
positionerClassName?: string;
|
||||
onConfirm: () => void | Promise<void>;
|
||||
onConfirmError?: (error: unknown) => void;
|
||||
}
|
||||
|
||||
export function ConfirmDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
title,
|
||||
description,
|
||||
cancelLabel,
|
||||
confirmLabel,
|
||||
loadingLabel,
|
||||
isLoading = false,
|
||||
confirmVariant = "destructive",
|
||||
contentClassName = "max-w-sm",
|
||||
overlayClassName,
|
||||
positionerClassName,
|
||||
onConfirm,
|
||||
onConfirmError,
|
||||
}: ConfirmDialogProps) {
|
||||
const handleConfirm = async () => {
|
||||
try {
|
||||
await onConfirm();
|
||||
} catch (error) {
|
||||
onConfirmError?.(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(nextOpen) => {
|
||||
if (!isLoading) {
|
||||
onOpenChange(nextOpen);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
className={contentClassName}
|
||||
overlayClassName={overlayClassName}
|
||||
positionerClassName={positionerClassName}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={isLoading}
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
{cancelLabel}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant={confirmVariant}
|
||||
disabled={isLoading}
|
||||
onClick={() => void handleConfirm()}
|
||||
>
|
||||
{isLoading && loadingLabel ? loadingLabel : confirmLabel}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -47,17 +47,24 @@ function DialogOverlay({
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
overlayClassName,
|
||||
positionerClassName,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
overlayClassName?: string;
|
||||
positionerClassName?: string;
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogOverlay className={overlayClassName} />
|
||||
<div
|
||||
data-slot="dialog-positioner"
|
||||
className="pointer-events-none fixed inset-0 z-[61] grid place-items-center p-4"
|
||||
className={cn(
|
||||
"pointer-events-none fixed inset-0 z-[61] grid place-items-center p-4",
|
||||
positionerClassName,
|
||||
)}
|
||||
>
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
|
||||
Reference in New Issue
Block a user