Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- Introduced turbopack alias for canvas in next.config.mjs. - Updated package.json scripts for development and branding tasks. - Added new dependencies for Tiptap extensions. - Implemented new demo layouts for agenda, contacts, drive, and mail applications. - Enhanced globals.css for improved theming and splash screen animations. - Added OAuth callback handling for drive mounts. - Updated layout components to integrate new demo shells and improve structure.
217 lines
9.3 KiB
TypeScript
217 lines
9.3 KiB
TypeScript
'use client'
|
|
|
|
import { useMemo, useState } from 'react'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Label } from '@/components/ui/label'
|
|
import { Play } from 'lucide-react'
|
|
import { useSimulateMailRule } from '@/lib/api/hooks/use-mail-automation-queries'
|
|
import type { RuleEditorState, RuleSimulationResult } from '@/lib/mail-automation/types'
|
|
import {
|
|
DEFAULT_SIMULATION_CALENDAR_EVENT,
|
|
DEFAULT_SIMULATION_CONTACT,
|
|
DEFAULT_SIMULATION_DRIVE_FILE,
|
|
DEFAULT_SIMULATION_MESSAGE,
|
|
workflowToApiPayload,
|
|
} from '@/lib/mail-automation/defaults'
|
|
import { inferDomainsFromTriggers } from '@/lib/mail-automation/domains'
|
|
import { AUTOMATION_DOMAIN_LABELS } from '@/lib/mail-automation/domains'
|
|
|
|
interface RuleSimulatorPanelProps {
|
|
state: RuleEditorState
|
|
ruleId?: string
|
|
}
|
|
|
|
export function RuleSimulatorPanel({ state, ruleId }: RuleSimulatorPanelProps) {
|
|
const simulate = useSimulateMailRule()
|
|
const [message, setMessage] = useState(DEFAULT_SIMULATION_MESSAGE)
|
|
const [driveFile, setDriveFile] = useState(DEFAULT_SIMULATION_DRIVE_FILE)
|
|
const [contact, setContact] = useState(DEFAULT_SIMULATION_CONTACT)
|
|
const [calendarEvent, setCalendarEvent] = useState(DEFAULT_SIMULATION_CALENDAR_EVENT)
|
|
const [result, setResult] = useState<RuleSimulationResult | null>(null)
|
|
|
|
const domains = useMemo(
|
|
() => inferDomainsFromTriggers(state.workflow.triggers),
|
|
[state.workflow.triggers]
|
|
)
|
|
|
|
async function runSimulation() {
|
|
const payload = workflowToApiPayload(state)
|
|
const res = await simulate.mutateAsync({
|
|
message,
|
|
...(ruleId
|
|
? { rule_id: ruleId }
|
|
: {
|
|
rule: {
|
|
conditions: payload.conditions,
|
|
actions: payload.actions,
|
|
workflow: payload.workflow,
|
|
},
|
|
}),
|
|
})
|
|
setResult(res)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-3 rounded-lg border border-border bg-muted/10 p-3">
|
|
<p className="text-xs font-medium">
|
|
Tester avec un événement exemple
|
|
{domains.length > 0 ? (
|
|
<span className="ml-1 font-normal text-muted-foreground">
|
|
({domains.map((d) => AUTOMATION_DOMAIN_LABELS[d]).join(', ')})
|
|
</span>
|
|
) : null}
|
|
</p>
|
|
|
|
{domains.includes('mail') ? (
|
|
<div className="space-y-2 rounded-md border border-border/50 p-2">
|
|
<p className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">Mail</p>
|
|
<div className="grid gap-2 sm:grid-cols-2">
|
|
<div>
|
|
<Label className="text-[10px]">De</Label>
|
|
<Input className="h-8 text-xs" value={message.from} onChange={(e) => setMessage({ ...message, from: e.target.value })} />
|
|
</div>
|
|
<div>
|
|
<Label className="text-[10px]">Sujet</Label>
|
|
<Input className="h-8 text-xs" value={message.subject} onChange={(e) => setMessage({ ...message, subject: e.target.value })} />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<Label className="text-[10px]">Corps</Label>
|
|
<textarea
|
|
className="mt-1 min-h-16 w-full rounded-md border border-input bg-background px-2 py-1.5 text-xs"
|
|
value={message.body_text}
|
|
onChange={(e) => setMessage({ ...message, body_text: e.target.value })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
{domains.includes('drive') ? (
|
|
<div className="space-y-2 rounded-md border border-border/50 p-2">
|
|
<p className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">Drive</p>
|
|
<div className="grid gap-2 sm:grid-cols-2">
|
|
<div>
|
|
<Label className="text-[10px]">Nom fichier</Label>
|
|
<Input className="h-8 text-xs" value={driveFile.file_name} onChange={(e) => setDriveFile({ ...driveFile, file_name: e.target.value })} />
|
|
</div>
|
|
<div>
|
|
<Label className="text-[10px]">Chemin</Label>
|
|
<Input className="h-8 text-xs" value={driveFile.file_path} onChange={(e) => setDriveFile({ ...driveFile, file_path: e.target.value })} />
|
|
</div>
|
|
<div>
|
|
<Label className="text-[10px]">Type MIME</Label>
|
|
<Input className="h-8 text-xs" value={driveFile.mime_type} onChange={(e) => setDriveFile({ ...driveFile, mime_type: e.target.value })} />
|
|
</div>
|
|
<div>
|
|
<Label className="text-[10px]">Taille (octets)</Label>
|
|
<Input type="number" className="h-8 text-xs" value={driveFile.file_size} onChange={(e) => setDriveFile({ ...driveFile, file_size: Number(e.target.value) || 0 })} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
{domains.includes('contacts') ? (
|
|
<div className="space-y-2 rounded-md border border-border/50 p-2">
|
|
<p className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">Contacts</p>
|
|
<div className="grid gap-2 sm:grid-cols-2">
|
|
<div>
|
|
<Label className="text-[10px]">Nom</Label>
|
|
<Input className="h-8 text-xs" value={contact.name} onChange={(e) => setContact({ ...contact, name: e.target.value })} />
|
|
</div>
|
|
<div>
|
|
<Label className="text-[10px]">E-mail</Label>
|
|
<Input className="h-8 text-xs" value={contact.email} onChange={(e) => setContact({ ...contact, email: e.target.value })} />
|
|
</div>
|
|
<div>
|
|
<Label className="text-[10px]">Téléphone</Label>
|
|
<Input className="h-8 text-xs" value={contact.phone} onChange={(e) => setContact({ ...contact, phone: e.target.value })} />
|
|
</div>
|
|
<div>
|
|
<Label className="text-[10px]">Organisation</Label>
|
|
<Input className="h-8 text-xs" value={contact.org} onChange={(e) => setContact({ ...contact, org: e.target.value })} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
{domains.includes('agenda') ? (
|
|
<div className="space-y-2 rounded-md border border-border/50 p-2">
|
|
<p className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">Agenda</p>
|
|
<div className="grid gap-2 sm:grid-cols-2">
|
|
<div>
|
|
<Label className="text-[10px]">Titre</Label>
|
|
<Input className="h-8 text-xs" value={calendarEvent.title} onChange={(e) => setCalendarEvent({ ...calendarEvent, title: e.target.value })} />
|
|
</div>
|
|
<div>
|
|
<Label className="text-[10px]">Lieu</Label>
|
|
<Input className="h-8 text-xs" value={calendarEvent.location} onChange={(e) => setCalendarEvent({ ...calendarEvent, location: e.target.value })} />
|
|
</div>
|
|
<div>
|
|
<Label className="text-[10px]">Organisateur</Label>
|
|
<Input className="h-8 text-xs" value={calendarEvent.organizer} onChange={(e) => setCalendarEvent({ ...calendarEvent, organizer: e.target.value })} />
|
|
</div>
|
|
<div>
|
|
<Label className="text-[10px]">Participant</Label>
|
|
<Input className="h-8 text-xs" value={calendarEvent.attendee} onChange={(e) => setCalendarEvent({ ...calendarEvent, attendee: e.target.value })} />
|
|
</div>
|
|
<div>
|
|
<Label className="text-[10px]">Agenda</Label>
|
|
<Input className="h-8 text-xs" value={calendarEvent.calendar_id} onChange={(e) => setCalendarEvent({ ...calendarEvent, calendar_id: e.target.value })} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
{domains.includes('drive') || domains.includes('contacts') || domains.includes('agenda') ? (
|
|
<p className="text-[10px] text-muted-foreground italic">
|
|
La simulation API reste orientée mail ; l'exécution réelle Drive, contacts et agenda est active
|
|
côté serveur à la réception des événements.
|
|
</p>
|
|
) : null}
|
|
|
|
<Button type="button" size="sm" disabled={simulate.isPending} onClick={runSimulation}>
|
|
<Play className="mr-1 size-3.5" />
|
|
Simuler
|
|
</Button>
|
|
|
|
{result ? (
|
|
<div className="space-y-2 rounded-md border border-border/60 bg-background p-2 text-xs">
|
|
<p>
|
|
Correspondance :{' '}
|
|
<span className={result.matched ? 'text-emerald-600' : 'text-muted-foreground'}>
|
|
{result.matched ? 'oui' : 'non'}
|
|
</span>
|
|
</p>
|
|
{result.steps?.length ? (
|
|
<div>
|
|
<p className="font-medium">Parcours</p>
|
|
<ol className="mt-1 list-decimal pl-4 text-muted-foreground">
|
|
{result.steps.map((s, i) => (
|
|
<li key={i}>
|
|
{s.node_type}
|
|
{s.handle ? ` → ${s.handle}` : ''}
|
|
</li>
|
|
))}
|
|
</ol>
|
|
</div>
|
|
) : null}
|
|
{result.actions?.length ? (
|
|
<div>
|
|
<p className="font-medium">Actions</p>
|
|
<ul className="mt-1 space-y-0.5 text-muted-foreground">
|
|
{result.actions.map((a, i) => (
|
|
<li key={i}>
|
|
{a.type}
|
|
{a.value ? `: ${a.value}` : ''} {a.ok ? '✓' : `✗ ${a.error ?? ''}`}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
)
|
|
}
|