CustomCrmSection.tsx4.5 KBView on GitHub 'use client';
import { useMutation, useQuery } from '@tanstack/react-query';
import { useTRPC } from '@/providers/query-provider';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { useEffect, useState } from 'react';
import { Database } from 'lucide-react';
import { toast } from 'sonner';
type CustomCrmSectionProps = {
userId?: string;
};
export function CustomCrmSection({ userId }: CustomCrmSectionProps) {
const trpc = useTRPC();
const [url, setUrl] = useState('');
const [secret, setSecret] = useState('');
const [secretHeader, setSecretHeader] = useState('X-Cedar-Secret');
const { data, refetch, isLoading } = useQuery(
trpc.connections.getWebhookExportConfig.queryOptions(userId ? { userId } : undefined),
);
const { mutateAsync: updateWebhookExportConfig, isPending } = useMutation(
trpc.connections.updateWebhookExportConfig.mutationOptions(),
);
useEffect(() => {
if (!data) return;
setUrl(data.url ?? '');
setSecret('');
setSecretHeader(data.secretHeader || 'X-Cedar-Secret');
}, [data]);
const handleSave = async () => {
if (!url.trim()) {
toast.error('Enter a Custom CRM endpoint URL');
return;
}
try {
await updateWebhookExportConfig({
url: url.trim(),
secret=[redacted] || undefined,
secretHeader: secretHeader.trim() || undefined,
userId,
});
setSecret('');
await refetch();
} catch (error) {
console.error('Failed to save Custom CRM config:', error);
toast.error('Failed to save Custom CRM endpoint');
}
};
const handleClear = async () => {
try {
await updateWebhookExportConfig({ url: null, userId });
setUrl('');
setSecret('');
setSecretHeader('X-Cedar-Secret');
await refetch();
} catch (error) {
console.error('Failed to clear Custom CRM config:', error);
toast.error('Failed to clear Custom CRM endpoint');
}
};
return (
<div className="space-y-4">
<div className="flex items-start justify-between gap-4">
<div className="flex items-center gap-3">
<div className="bg-primary/10 flex h-10 w-10 shrink-0 items-center justify-center rounded-lg">
<Database className="text-primary h-5 w-5" />
</div>
<div>
<h3 className="text-base font-semibold">Custom CRM</h3>
<p className="text-muted-foreground text-sm">
Send Cedar conversation field updates to a custom CRM webhook.
</p>
</div>
</div>
<Badge variant={data?.configured ? 'default' : 'secondary'}>
{data?.configured ? 'Connected' : 'Not configured'}
</Badge>
</div>
<div className="space-y-4">
<div className="grid gap-2">
<Label htmlFor="custom-crm-url">Endpoint URL</Label>
<Input
id="custom-crm-url"
value={url}
onChange={(event) => setUrl(event.target.value)}
placeholder="https://example.com/webhooks/cedar-update-deal"
disabled={isLoading || isPending}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="custom-crm-secret">Secret</Label>
<Input
id="custom-crm-secret"
type="password"
value={secret}
onChange={(event) => setSecret(event.target.value)}
placeholder={data?.hasSecret ? 'Leave blank to keep existing secret' : 'Secret value'}
disabled={isLoading || isPending}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="custom-crm-secret-header">Secret header</Label>
<Input
id="custom-crm-secret-header"
value={secretHeader}
onChange={(event) => setSecretHeader(event.target.value)}
placeholder="X-Cedar-Secret"
disabled={isLoading || isPending}
/>
</div>
<div className="flex justify-end gap-2">
<Button
type="button"
variant="outline"
onClick={handleClear}
disabled={isLoading || isPending || !data?.configured}
>
Clear
</Button>
<Button type="button" onClick={handleSave} disabled={isLoading || isPending}>
Save
</Button>
</div>
</div>
</div>
);
}