refactor: convert forwardRef components to function components for consistency
This commit is contained in:
@@ -106,14 +106,18 @@ export default function CampaignDetailsPage() {
|
||||
);
|
||||
|
||||
const [editedCampaign, setEditedCampaign] = useState<Partial<Campaign>>({});
|
||||
const [isScheduleDialogOpen, setIsScheduleDialogOpen] = useState(false);
|
||||
const [scheduledDateTime, setScheduledDateTime] = useState('');
|
||||
const [isTestEmailDialogOpen, setIsTestEmailDialogOpen] = useState(false);
|
||||
const [testEmailAddress, setTestEmailAddress] = useState('');
|
||||
const [sendingTestEmail, setSendingTestEmail] = useState(false);
|
||||
const [showCancelDialog, setShowCancelDialog] = useState(false);
|
||||
const [showSendDialog, setShowSendDialog] = useState(false);
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
|
||||
type CampaignDialog =
|
||||
| {type: 'none'}
|
||||
| {type: 'schedule'}
|
||||
| {type: 'testEmail'; sending: boolean}
|
||||
| {type: 'send'}
|
||||
| {type: 'cancel'}
|
||||
| {type: 'delete'};
|
||||
|
||||
const [dialog, setDialog] = useState<CampaignDialog>({type: 'none'});
|
||||
|
||||
// Automatically initialize edit fields when campaign is loaded and is a draft
|
||||
const isEditMode = campaign?.data.status === CampaignStatus.DRAFT;
|
||||
@@ -172,7 +176,7 @@ export default function CampaignDetailsPage() {
|
||||
// Show confirmation with user's local time
|
||||
const localTimeString = formatFullDateTime(scheduledDate);
|
||||
toast.success(`Campaign scheduled for ${localTimeString}`);
|
||||
setIsScheduleDialogOpen(false);
|
||||
setDialog({type: 'none'});
|
||||
setScheduledDateTime('');
|
||||
void mutate();
|
||||
} catch (error) {
|
||||
@@ -186,7 +190,7 @@ export default function CampaignDetailsPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
setSendingTestEmail(true);
|
||||
setDialog({type: 'testEmail', sending: true});
|
||||
|
||||
try {
|
||||
await network.fetch<{success: boolean; message: string}>('POST', `/campaigns/${id}/test`, {
|
||||
@@ -194,12 +198,12 @@ export default function CampaignDetailsPage() {
|
||||
} as any);
|
||||
|
||||
toast.success(`Test email sent to ${testEmailAddress}`);
|
||||
setIsTestEmailDialogOpen(false);
|
||||
setDialog({type: 'none'});
|
||||
setTestEmailAddress('');
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to send test email');
|
||||
} finally {
|
||||
setSendingTestEmail(false);
|
||||
setDialog(d => (d.type === 'testEmail' ? {type: 'testEmail', sending: false} : d));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -370,7 +374,7 @@ export default function CampaignDetailsPage() {
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
onClick={() => setShowDeleteDialog(true)}
|
||||
onClick={() => setDialog({type: 'delete'})}
|
||||
className="flex-1 sm:flex-none"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
@@ -395,7 +399,7 @@ export default function CampaignDetailsPage() {
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-72">
|
||||
<DropdownMenuItem onClick={() => setIsTestEmailDialogOpen(true)} className="py-3 cursor-pointer">
|
||||
<DropdownMenuItem onClick={() => setDialog({type: 'testEmail', sending: false})} className="py-3 cursor-pointer">
|
||||
<div className="flex items-start gap-3">
|
||||
<TestTube className="h-4 w-4 mt-0.5 text-neutral-700" />
|
||||
<div className="flex flex-col gap-0.5 flex-1">
|
||||
@@ -406,7 +410,7 @@ export default function CampaignDetailsPage() {
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setShowSendDialog(true)} className="py-3 cursor-pointer">
|
||||
<DropdownMenuItem onClick={() => setDialog({type: 'send'})} className="py-3 cursor-pointer">
|
||||
<div className="flex items-start gap-3">
|
||||
<Send className="h-4 w-4 mt-0.5 text-neutral-700" />
|
||||
<div className="flex flex-col gap-0.5 flex-1">
|
||||
@@ -417,7 +421,7 @@ export default function CampaignDetailsPage() {
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setIsScheduleDialogOpen(true)} className="py-3 cursor-pointer">
|
||||
<DropdownMenuItem onClick={() => setDialog({type: 'schedule'})} className="py-3 cursor-pointer">
|
||||
<div className="flex items-start gap-3">
|
||||
<Calendar className="h-4 w-4 mt-0.5 text-neutral-700" />
|
||||
<div className="flex flex-col gap-0.5 flex-1">
|
||||
@@ -531,7 +535,6 @@ export default function CampaignDetailsPage() {
|
||||
onFromNameChange={value => setEditedCampaign({...editedCampaign, fromName: value})}
|
||||
onReplyToChange={value => setEditedCampaign({...editedCampaign, replyTo: value})}
|
||||
fromNamePlaceholder={activeProject?.name || 'Your Company'}
|
||||
showFromNameHelpText
|
||||
layout="vertical"
|
||||
/>
|
||||
</CardContent>
|
||||
@@ -658,7 +661,7 @@ export default function CampaignDetailsPage() {
|
||||
</Card>
|
||||
|
||||
{/* Test Email Dialog */}
|
||||
<Dialog open={isTestEmailDialogOpen} onOpenChange={setIsTestEmailDialogOpen}>
|
||||
<Dialog open={dialog.type === 'testEmail'} onOpenChange={open => !open && setDialog({type: 'none'})}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Send Test Email</DialogTitle>
|
||||
@@ -695,21 +698,25 @@ export default function CampaignDetailsPage() {
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setIsTestEmailDialogOpen(false);
|
||||
setDialog({type: 'none'});
|
||||
setTestEmailAddress('');
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="button" onClick={handleSendTestEmail} disabled={sendingTestEmail || !testEmailAddress}>
|
||||
{sendingTestEmail ? 'Sending...' : 'Send Test Email'}
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleSendTestEmail}
|
||||
disabled={(dialog.type === 'testEmail' && dialog.sending) || !testEmailAddress}
|
||||
>
|
||||
{dialog.type === 'testEmail' && dialog.sending ? 'Sending...' : 'Send Test Email'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Schedule Dialog */}
|
||||
<Dialog open={isScheduleDialogOpen} onOpenChange={setIsScheduleDialogOpen}>
|
||||
<Dialog open={dialog.type === 'schedule'} onOpenChange={open => !open && setDialog({type: 'none'})}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Schedule Campaign</DialogTitle>
|
||||
@@ -801,7 +808,7 @@ export default function CampaignDetailsPage() {
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setIsScheduleDialogOpen(false);
|
||||
setDialog({type: 'none'});
|
||||
setScheduledDateTime('');
|
||||
}}
|
||||
>
|
||||
@@ -816,11 +823,11 @@ export default function CampaignDetailsPage() {
|
||||
</form>
|
||||
|
||||
{/* Sticky Save Bar */}
|
||||
<StickySaveBar hasChanges={hasChanges} isSubmitting={isSubmitting} onSave={handleSave} />
|
||||
<StickySaveBar status={isSubmitting ? 'saving' : hasChanges ? 'dirty' : 'idle'} onSave={handleSave} />
|
||||
|
||||
<ConfirmDialog
|
||||
open={showSendDialog}
|
||||
onOpenChange={setShowSendDialog}
|
||||
open={dialog.type === 'send'}
|
||||
onOpenChange={open => !open && setDialog({type: 'none'})}
|
||||
onConfirm={handleSend}
|
||||
title="Send Campaign"
|
||||
description="Are you sure you want to send this campaign now? This action cannot be undone."
|
||||
@@ -829,8 +836,8 @@ export default function CampaignDetailsPage() {
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
open={showDeleteDialog}
|
||||
onOpenChange={setShowDeleteDialog}
|
||||
open={dialog.type === 'delete'}
|
||||
onOpenChange={open => !open && setDialog({type: 'none'})}
|
||||
onConfirm={handleDelete}
|
||||
title="Delete Campaign"
|
||||
description="Are you sure you want to delete this draft campaign? This action cannot be undone."
|
||||
@@ -864,7 +871,7 @@ export default function CampaignDetailsPage() {
|
||||
{/* Actions */}
|
||||
{(c.status === CampaignStatus.SCHEDULED || c.status === CampaignStatus.SENDING) && (
|
||||
<div className="flex justify-end">
|
||||
<Button variant="destructive" onClick={() => setShowCancelDialog(true)} className="w-full sm:w-auto">
|
||||
<Button variant="destructive" onClick={() => setDialog({type: 'cancel'})} className="w-full sm:w-auto">
|
||||
<XCircle className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Cancel Campaign</span>
|
||||
<span className="sm:hidden">Cancel</span>
|
||||
@@ -1069,8 +1076,8 @@ export default function CampaignDetailsPage() {
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
open={showCancelDialog}
|
||||
onOpenChange={setShowCancelDialog}
|
||||
open={dialog.type === 'cancel'}
|
||||
onOpenChange={open => !open && setDialog({type: 'none'})}
|
||||
onConfirm={handleCancel}
|
||||
title="Cancel Campaign"
|
||||
description="Are you sure you want to cancel this campaign?"
|
||||
|
||||
@@ -92,11 +92,11 @@ export default function Settings() {
|
||||
const {data: user} = useUser();
|
||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [showRegenerateDialog, setShowRegenerateDialog] = useState(false);
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const [showResetDialog, setShowResetDialog] = useState(false);
|
||||
const [deleteConfirmText, setDeleteConfirmText] = useState('');
|
||||
const [resetConfirmText, setResetConfirmText] = useState('');
|
||||
|
||||
type SettingsDialog = {type: 'none'} | {type: 'regenerate'} | {type: 'delete'} | {type: 'reset'};
|
||||
const [dialog, setDialog] = useState<SettingsDialog>({type: 'none'});
|
||||
const [isLoadingBilling, setIsLoadingBilling] = useState(false);
|
||||
const [selectedCurrency, setSelectedCurrency] = useState<string>('auto');
|
||||
const [showCurrencySelector, setShowCurrencySelector] = useState(false);
|
||||
@@ -235,18 +235,18 @@ export default function Settings() {
|
||||
await projectsMutate();
|
||||
|
||||
setSuccessMessage('API keys regenerated successfully');
|
||||
setShowRegenerateDialog(false);
|
||||
setDialog({type: 'none'});
|
||||
|
||||
// Clear success message after 3 seconds
|
||||
setTimeout(() => setSuccessMessage(null), 3000);
|
||||
} catch (error) {
|
||||
setErrorMessage(error instanceof Error ? error.message : 'Failed to regenerate API keys');
|
||||
setShowRegenerateDialog(false);
|
||||
setDialog({type: 'none'});
|
||||
}
|
||||
};
|
||||
|
||||
const promptRegenerateKeys = () => {
|
||||
setShowRegenerateDialog(true);
|
||||
setDialog({type: 'regenerate'});
|
||||
};
|
||||
|
||||
const handleStartSubscription = async (currency: string = 'auto') => {
|
||||
@@ -314,7 +314,7 @@ export default function Settings() {
|
||||
await network.fetch('POST', `/users/@me/projects/${activeProject.id}/reset`);
|
||||
|
||||
setSuccessMessage('Project reset successfully. All data has been cleared.');
|
||||
setShowResetDialog(false);
|
||||
setDialog({type: 'none'});
|
||||
setResetConfirmText('');
|
||||
|
||||
// Refresh the page to reload data
|
||||
@@ -323,7 +323,7 @@ export default function Settings() {
|
||||
}, 1500);
|
||||
} catch (error) {
|
||||
setErrorMessage(error instanceof Error ? error.message : 'Failed to reset project');
|
||||
setShowResetDialog(false);
|
||||
setDialog({type: 'none'});
|
||||
setResetConfirmText('');
|
||||
}
|
||||
};
|
||||
@@ -338,7 +338,7 @@ export default function Settings() {
|
||||
await network.fetch('DELETE', `/users/@me/projects/${activeProject.id}`);
|
||||
|
||||
setSuccessMessage('Project deleted successfully. Redirecting...');
|
||||
setShowDeleteDialog(false);
|
||||
setDialog({type: 'none'});
|
||||
setDeleteConfirmText('');
|
||||
|
||||
// Refresh projects list and redirect to dashboard
|
||||
@@ -350,7 +350,7 @@ export default function Settings() {
|
||||
}, 1500);
|
||||
} catch (error) {
|
||||
setErrorMessage(error instanceof Error ? error.message : 'Failed to delete project');
|
||||
setShowDeleteDialog(false);
|
||||
setDialog({type: 'none'});
|
||||
setDeleteConfirmText('');
|
||||
}
|
||||
};
|
||||
@@ -587,7 +587,7 @@ export default function Settings() {
|
||||
<span>API keys, domains, billing information</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="button" variant="outline" onClick={() => setShowResetDialog(true)} className="shrink-0">
|
||||
<Button type="button" variant="outline" onClick={() => setDialog({type: 'reset'})} className="shrink-0">
|
||||
Reset Data
|
||||
</Button>
|
||||
</div>
|
||||
@@ -614,7 +614,7 @@ export default function Settings() {
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
onClick={() => setShowDeleteDialog(true)}
|
||||
onClick={() => setDialog({type: 'delete'})}
|
||||
className="shrink-0"
|
||||
>
|
||||
Delete Project
|
||||
@@ -753,7 +753,7 @@ export default function Settings() {
|
||||
{/* Billing Limits */}
|
||||
<BillingLimits
|
||||
projectId={activeProject.id}
|
||||
hasSubscription={!!activeProject.subscription}
|
||||
tier={activeProject.subscription ? 'paid' : 'free'}
|
||||
billingEnabled={billingEnabled}
|
||||
/>
|
||||
|
||||
@@ -814,7 +814,7 @@ export default function Settings() {
|
||||
</div>
|
||||
|
||||
{/* Regenerate Keys Confirmation Dialog */}
|
||||
<Dialog open={showRegenerateDialog} onOpenChange={setShowRegenerateDialog}>
|
||||
<Dialog open={dialog.type === 'regenerate'} onOpenChange={open => !open && setDialog({type: 'none'})}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
@@ -833,7 +833,7 @@ export default function Settings() {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowRegenerateDialog(false)}>
|
||||
<Button variant="outline" onClick={() => setDialog({type: 'none'})}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={handleRegenerateKeys}>
|
||||
@@ -844,7 +844,7 @@ export default function Settings() {
|
||||
</Dialog>
|
||||
|
||||
{/* Reset Project Confirmation Dialog */}
|
||||
<Dialog open={showResetDialog} onOpenChange={setShowResetDialog}>
|
||||
<Dialog open={dialog.type === 'reset'} onOpenChange={open => !open && setDialog({type: 'none'})}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader className="space-y-3">
|
||||
<div className="mx-auto w-12 h-12 bg-amber-100 rounded-full flex items-center justify-center">
|
||||
@@ -876,7 +876,7 @@ export default function Settings() {
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setShowResetDialog(false);
|
||||
setDialog({type: 'none'});
|
||||
setResetConfirmText('');
|
||||
}}
|
||||
className="w-full"
|
||||
@@ -896,7 +896,7 @@ export default function Settings() {
|
||||
</Dialog>
|
||||
|
||||
{/* Delete Project Confirmation Dialog */}
|
||||
<Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<Dialog open={dialog.type === 'delete'} onOpenChange={open => !open && setDialog({type: 'none'})}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader className="space-y-3">
|
||||
<div className="mx-auto w-12 h-12 bg-red-100 rounded-full flex items-center justify-center">
|
||||
@@ -934,7 +934,7 @@ export default function Settings() {
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setShowDeleteDialog(false);
|
||||
setDialog({type: 'none'});
|
||||
setDeleteConfirmText('');
|
||||
}}
|
||||
className="w-full"
|
||||
|
||||
@@ -21,7 +21,7 @@ import {ArrowLeft, Save, Trash2, TriangleAlert} from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import {NextSeo} from 'next-seo';
|
||||
import {useRouter} from 'next/router';
|
||||
import {useEffect, useState} from 'react';
|
||||
import {useEffect, useMemo, useState} from 'react';
|
||||
import {toast} from 'sonner';
|
||||
import useSWR from 'swr';
|
||||
import {TemplateSchemas, detectUnsubscribeSignal} from '@plunk/shared';
|
||||
@@ -38,7 +38,6 @@ export default function TemplateEditorPage() {
|
||||
|
||||
const [editedTemplate, setEditedTemplate] = useState<Partial<Template>>({});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
|
||||
// Initialize edit fields when template loads
|
||||
@@ -54,16 +53,12 @@ export default function TemplateEditorPage() {
|
||||
replyTo: template.replyTo || '',
|
||||
type: template.type,
|
||||
});
|
||||
// Reset hasChanges when loading fresh data
|
||||
setHasChanges(false);
|
||||
}
|
||||
}, [template, editedTemplate]);
|
||||
|
||||
// Track changes
|
||||
useEffect(() => {
|
||||
if (!template || Object.keys(editedTemplate).length === 0) return;
|
||||
|
||||
const changed =
|
||||
const hasChanges = useMemo(() => {
|
||||
if (!template || Object.keys(editedTemplate).length === 0) return false;
|
||||
return (
|
||||
editedTemplate.name !== template.name ||
|
||||
(editedTemplate.description || '') !== (template.description || '') ||
|
||||
editedTemplate.subject !== template.subject ||
|
||||
@@ -71,9 +66,8 @@ export default function TemplateEditorPage() {
|
||||
editedTemplate.from !== template.from ||
|
||||
(editedTemplate.fromName || '') !== (template.fromName || '') ||
|
||||
(editedTemplate.replyTo || '') !== (template.replyTo || '') ||
|
||||
editedTemplate.type !== template.type;
|
||||
|
||||
setHasChanges(changed);
|
||||
editedTemplate.type !== template.type
|
||||
);
|
||||
}, [editedTemplate, template]);
|
||||
|
||||
// Warn before leaving page with unsaved changes
|
||||
@@ -96,7 +90,6 @@ export default function TemplateEditorPage() {
|
||||
});
|
||||
|
||||
// Silent save - no toast notification
|
||||
setHasChanges(false);
|
||||
void mutate();
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to save template');
|
||||
@@ -267,7 +260,6 @@ export default function TemplateEditorPage() {
|
||||
onFromNameChange={value => setEditedTemplate({...editedTemplate, fromName: value})}
|
||||
onReplyToChange={value => setEditedTemplate({...editedTemplate, replyTo: value})}
|
||||
fromNamePlaceholder={activeProject?.name || 'Your Company'}
|
||||
showFromNameHelpText
|
||||
layout="vertical"
|
||||
/>
|
||||
</CardContent>
|
||||
@@ -290,7 +282,7 @@ export default function TemplateEditorPage() {
|
||||
</form>
|
||||
|
||||
{/* Sticky Save Bar */}
|
||||
<StickySaveBar hasChanges={hasChanges} isSubmitting={isSubmitting} onSave={handleSave} />
|
||||
<StickySaveBar status={isSubmitting ? 'saving' : hasChanges ? 'dirty' : 'idle'} onSave={handleSave} />
|
||||
|
||||
{/* Delete Template Confirmation */}
|
||||
<ConfirmDialog
|
||||
|
||||
@@ -74,12 +74,16 @@ export default function WorkflowEditorPage() {
|
||||
const router = useRouter();
|
||||
const {id} = router.query;
|
||||
const [activeTab, setActiveTab] = useState<'builder' | 'executions'>('builder');
|
||||
const [showSettingsDialog, setShowSettingsDialog] = useState(false);
|
||||
const [editingStep, setEditingStep] = useState<WorkflowStep | null>(null);
|
||||
const [showCancelAllDialog, setShowCancelAllDialog] = useState(false);
|
||||
const [executionToCancel, setExecutionToCancel] = useState<string | null>(null);
|
||||
const [isCancelling, setIsCancelling] = useState(false);
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
|
||||
type WorkflowDialog =
|
||||
| {type: 'none'}
|
||||
| {type: 'settings'}
|
||||
| {type: 'cancelAll'; cancelling: boolean}
|
||||
| {type: 'cancelOne'; executionId: string; cancelling: boolean}
|
||||
| {type: 'editStep'; step: WorkflowStep}
|
||||
| {type: 'delete'};
|
||||
|
||||
const [dialog, setDialog] = useState<WorkflowDialog>({type: 'none'});
|
||||
|
||||
const {data: workflow, mutate} = useSWR<WorkflowWithDetails>(id ? `/workflows/${id}` : null, {
|
||||
revalidateOnFocus: false,
|
||||
@@ -106,31 +110,29 @@ export default function WorkflowEditorPage() {
|
||||
|
||||
// Handler for cancelling a single execution
|
||||
const handleCancelExecution = async (executionId: string) => {
|
||||
setIsCancelling(true);
|
||||
setDialog({type: 'cancelOne', executionId, cancelling: true});
|
||||
try {
|
||||
await network.fetch('DELETE', `/workflows/${id}/executions/${executionId}`);
|
||||
toast.success('Execution cancelled successfully');
|
||||
setDialog({type: 'none'});
|
||||
void mutate();
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to cancel execution');
|
||||
} finally {
|
||||
setIsCancelling(false);
|
||||
setExecutionToCancel(null);
|
||||
setDialog({type: 'cancelOne', executionId, cancelling: false});
|
||||
}
|
||||
};
|
||||
|
||||
// Handler for cancelling all executions
|
||||
const handleCancelAllExecutions = async () => {
|
||||
setIsCancelling(true);
|
||||
setDialog(d => (d.type === 'cancelAll' ? {...d, cancelling: true} : d));
|
||||
try {
|
||||
const result = await network.fetch<{cancelled: number}>('POST', `/workflows/${id}/executions/cancel-all`);
|
||||
toast.success(`Successfully cancelled ${result.cancelled} execution(s)`);
|
||||
setDialog({type: 'none'});
|
||||
void mutate();
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to cancel executions');
|
||||
} finally {
|
||||
setIsCancelling(false);
|
||||
setShowCancelAllDialog(false);
|
||||
setDialog(d => (d.type === 'cancelAll' ? {...d, cancelling: false} : d));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -312,7 +314,7 @@ export default function WorkflowEditorPage() {
|
||||
await network.fetch<Workflow, typeof WorkflowSchemas.update>('PATCH', `/workflows/${id}`, data);
|
||||
toast.success('Workflow updated successfully');
|
||||
void mutate();
|
||||
setShowSettingsDialog(false);
|
||||
setDialog({type: 'none'});
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to update workflow');
|
||||
}
|
||||
@@ -336,13 +338,13 @@ export default function WorkflowEditorPage() {
|
||||
if (stepId && workflow) {
|
||||
const step = workflow.steps.find(s => s.id === stepId);
|
||||
if (step) {
|
||||
setEditingStep(step);
|
||||
setDialog({type: 'editStep', step});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenSettingsEvent = () => {
|
||||
setShowSettingsDialog(true);
|
||||
setDialog({type: 'settings'});
|
||||
};
|
||||
|
||||
window.addEventListener('workflow-edit-step', handleEditStepEvent);
|
||||
@@ -398,13 +400,13 @@ export default function WorkflowEditorPage() {
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 flex-shrink-0">
|
||||
<Button variant="ghost" size="icon" onClick={() => setShowSettingsDialog(true)} aria-label="Settings">
|
||||
<Button variant="ghost" size="icon" onClick={() => setDialog({type: 'settings'})} aria-label="Settings">
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setShowDeleteDialog(true)}
|
||||
onClick={() => setDialog({type: 'delete'})}
|
||||
aria-label="Delete workflow"
|
||||
className="text-neutral-400 hover:text-red-600 hover:bg-red-50"
|
||||
>
|
||||
@@ -542,7 +544,7 @@ export default function WorkflowEditorPage() {
|
||||
<CardDescription>View and manage all executions of this workflow</CardDescription>
|
||||
</div>
|
||||
{activeExecutionsCount > 0 && (
|
||||
<Button variant="outline" onClick={() => setShowCancelAllDialog(true)}>
|
||||
<Button variant="outline" onClick={() => setDialog({type: 'cancelAll', cancelling: false})}>
|
||||
Cancel All Active ({activeExecutionsCount})
|
||||
</Button>
|
||||
)}
|
||||
@@ -616,8 +618,8 @@ export default function WorkflowEditorPage() {
|
||||
<Button
|
||||
variant="destructiveGhost"
|
||||
size="sm"
|
||||
onClick={() => setExecutionToCancel(execution.id)}
|
||||
disabled={isCancelling}
|
||||
onClick={() => setDialog({type: 'cancelOne', executionId: execution.id, cancelling: false})}
|
||||
disabled={dialog.type === 'cancelOne' && dialog.cancelling}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
@@ -639,37 +641,38 @@ export default function WorkflowEditorPage() {
|
||||
<>
|
||||
<SettingsDialog
|
||||
workflow={workflow}
|
||||
open={showSettingsDialog}
|
||||
onOpenChange={setShowSettingsDialog}
|
||||
open={dialog.type === 'settings'}
|
||||
onOpenChange={open => !open && setDialog({type: 'none'})}
|
||||
onSave={handleUpdateSettings}
|
||||
/>
|
||||
{editingStep && (
|
||||
{dialog.type === 'editStep' && (
|
||||
<EditStepDialog
|
||||
step={editingStep}
|
||||
step={dialog.step}
|
||||
workflowId={id as string}
|
||||
open={!!editingStep}
|
||||
onOpenChange={open => !open && setEditingStep(null)}
|
||||
open={true}
|
||||
onOpenChange={open => !open && setDialog({type: 'none'})}
|
||||
onSuccess={() => mutate()}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Cancel Single Execution Confirmation */}
|
||||
<ConfirmDialog
|
||||
open={!!executionToCancel}
|
||||
onOpenChange={open => !open && setExecutionToCancel(null)}
|
||||
open={dialog.type === 'cancelOne'}
|
||||
onOpenChange={open => !open && setDialog({type: 'none'})}
|
||||
onConfirm={() => {
|
||||
if (executionToCancel) {
|
||||
return handleCancelExecution(executionToCancel);
|
||||
if (dialog.type === 'cancelOne') {
|
||||
return handleCancelExecution(dialog.executionId);
|
||||
}
|
||||
}}
|
||||
title="Cancel Execution"
|
||||
description={
|
||||
executionToCancel && executionsData?.executions ? (
|
||||
dialog.type === 'cancelOne' && executionsData?.executions ? (
|
||||
<div className="space-y-2">
|
||||
<p>
|
||||
Are you sure you want to cancel the workflow execution for{' '}
|
||||
<strong>
|
||||
{executionsData.executions.find(e => e.id === executionToCancel)?.contact.email || 'this contact'}
|
||||
{executionsData.executions.find(e => e.id === dialog.executionId)?.contact.email ||
|
||||
'this contact'}
|
||||
</strong>
|
||||
?
|
||||
</p>
|
||||
@@ -685,13 +688,13 @@ export default function WorkflowEditorPage() {
|
||||
confirmText="Cancel Execution"
|
||||
cancelText="Keep Running"
|
||||
variant="destructive"
|
||||
isLoading={isCancelling}
|
||||
status={dialog.type === 'cancelOne' && dialog.cancelling ? 'loading' : 'idle'}
|
||||
/>
|
||||
|
||||
{/* Cancel All Executions Confirmation */}
|
||||
<ConfirmDialog
|
||||
open={showCancelAllDialog}
|
||||
onOpenChange={setShowCancelAllDialog}
|
||||
open={dialog.type === 'cancelAll'}
|
||||
onOpenChange={open => !open && setDialog({type: 'none'})}
|
||||
onConfirm={handleCancelAllExecutions}
|
||||
title="Cancel All Active Executions"
|
||||
description={
|
||||
@@ -709,13 +712,13 @@ export default function WorkflowEditorPage() {
|
||||
confirmText={`Cancel ${activeExecutionsCount} Execution${activeExecutionsCount !== 1 ? 's' : ''}`}
|
||||
cancelText="Keep Running"
|
||||
variant="destructive"
|
||||
isLoading={isCancelling}
|
||||
status={dialog.type === 'cancelAll' && dialog.cancelling ? 'loading' : 'idle'}
|
||||
/>
|
||||
|
||||
{/* Delete Workflow Confirmation */}
|
||||
<ConfirmDialog
|
||||
open={showDeleteDialog}
|
||||
onOpenChange={setShowDeleteDialog}
|
||||
open={dialog.type === 'delete'}
|
||||
onOpenChange={open => !open && setDialog({type: 'none'})}
|
||||
onConfirm={handleDelete}
|
||||
title="Delete Workflow"
|
||||
description="Are you sure you want to delete this workflow? This action cannot be undone."
|
||||
|
||||
Reference in New Issue
Block a user