Fix for delay actions in workflows
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import {type Job, Queue} from 'bullmq';
|
import {type Job, Queue} from 'bullmq';
|
||||||
import type {RedisOptions} from 'ioredis';
|
import type {RedisOptions} from 'ioredis';
|
||||||
|
import signale from 'signale';
|
||||||
|
|
||||||
import {REDIS_URL} from '../app/constants.js';
|
import {REDIS_URL} from '../app/constants.js';
|
||||||
import {prisma} from '../database/prisma.js';
|
import {prisma} from '../database/prisma.js';
|
||||||
@@ -253,7 +254,7 @@ export class QueueService {
|
|||||||
|
|
||||||
if (job) {
|
if (job) {
|
||||||
await job.remove();
|
await job.remove();
|
||||||
console.log(`[QUEUE] Cancelled timeout job ${jobId}`);
|
signale.info(`[QUEUE] Cancelled timeout job ${jobId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,7 +436,7 @@ export class QueueService {
|
|||||||
* This should be called when a project is disabled
|
* This should be called when a project is disabled
|
||||||
*/
|
*/
|
||||||
public static async cancelAllProjectJobs(projectId: string): Promise<void> {
|
public static async cancelAllProjectJobs(projectId: string): Promise<void> {
|
||||||
console.log(`[QUEUE] Cancelling all pending jobs for project ${projectId}`);
|
signale.info(`[QUEUE] Cancelling all pending jobs for project ${projectId}`);
|
||||||
|
|
||||||
// Cancel all scheduled campaigns for this project
|
// Cancel all scheduled campaigns for this project
|
||||||
const scheduledCampaigns = await scheduledQueue.getJobs(['waiting', 'delayed']);
|
const scheduledCampaigns = await scheduledQueue.getJobs(['waiting', 'delayed']);
|
||||||
@@ -449,7 +450,7 @@ export class QueueService {
|
|||||||
|
|
||||||
if (campaign?.projectId === projectId) {
|
if (campaign?.projectId === projectId) {
|
||||||
await job.remove();
|
await job.remove();
|
||||||
console.log(`[QUEUE] Removed scheduled campaign job ${job.id}`);
|
signale.info(`[QUEUE] Removed scheduled campaign job ${job.id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,7 +464,7 @@ export class QueueService {
|
|||||||
|
|
||||||
if (email?.projectId === projectId) {
|
if (email?.projectId === projectId) {
|
||||||
await job.remove();
|
await job.remove();
|
||||||
console.log(`[QUEUE] Removed email job ${job.id}`);
|
signale.info(`[QUEUE] Removed email job ${job.id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -477,7 +478,7 @@ export class QueueService {
|
|||||||
|
|
||||||
if (campaign?.projectId === projectId) {
|
if (campaign?.projectId === projectId) {
|
||||||
await job.remove();
|
await job.remove();
|
||||||
console.log(`[QUEUE] Removed campaign batch job ${job.id}`);
|
signale.info(`[QUEUE] Removed campaign batch job ${job.id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -491,11 +492,11 @@ export class QueueService {
|
|||||||
|
|
||||||
if (execution?.workflow.projectId === projectId) {
|
if (execution?.workflow.projectId === projectId) {
|
||||||
await job.remove();
|
await job.remove();
|
||||||
console.log(`[QUEUE] Removed workflow step job ${job.id}`);
|
signale.info(`[QUEUE] Removed workflow step job ${job.id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[QUEUE] Finished cancelling jobs for project ${projectId}`);
|
signale.info(`[QUEUE] Finished cancelling jobs for project ${projectId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type {
|
|||||||
} from '@plunk/db';
|
} from '@plunk/db';
|
||||||
import {StepExecutionStatus, WorkflowExecutionStatus} from '@plunk/db';
|
import {StepExecutionStatus, WorkflowExecutionStatus} from '@plunk/db';
|
||||||
import {WorkflowStepConfigSchemas, renderTemplate} from '@plunk/shared';
|
import {WorkflowStepConfigSchemas, renderTemplate} from '@plunk/shared';
|
||||||
|
import signale from 'signale';
|
||||||
|
|
||||||
import {prisma} from '../database/prisma.js';
|
import {prisma} from '../database/prisma.js';
|
||||||
import {HttpException} from '../exceptions/index.js';
|
import {HttpException} from '../exceptions/index.js';
|
||||||
@@ -41,7 +42,9 @@ export class WorkflowExecutionService {
|
|||||||
* This is the main entry point for executing workflow steps
|
* This is the main entry point for executing workflow steps
|
||||||
*/
|
*/
|
||||||
public static async processStepExecution(executionId: string, stepId: string): Promise<void> {
|
public static async processStepExecution(executionId: string, stepId: string): Promise<void> {
|
||||||
const execution = await prisma.workflowExecution.findUnique({
|
signale.info(`[WORKFLOW] Processing step execution: ${executionId} -> ${stepId}`);
|
||||||
|
|
||||||
|
const initialExecution = await prisma.workflowExecution.findUnique({
|
||||||
where: {id: executionId},
|
where: {id: executionId},
|
||||||
include: {
|
include: {
|
||||||
workflow: {
|
workflow: {
|
||||||
@@ -64,18 +67,26 @@ export class WorkflowExecutionService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!execution) {
|
if (!initialExecution) {
|
||||||
|
signale.error(`[WORKFLOW] Execution ${executionId} not found`);
|
||||||
throw new HttpException(404, 'Workflow execution not found');
|
throw new HttpException(404, 'Workflow execution not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (execution.status !== WorkflowExecutionStatus.RUNNING) {
|
signale.info(`[WORKFLOW] Execution ${executionId} status: ${initialExecution.status}`);
|
||||||
|
|
||||||
|
// For WAITING executions, check if this is a delayed step that's ready to execute
|
||||||
|
if (initialExecution.status === WorkflowExecutionStatus.WAITING) {
|
||||||
|
signale.info(`[WORKFLOW] Execution ${executionId} is WAITING, resuming from delay`);
|
||||||
|
// This is a delayed step - continue with execution
|
||||||
|
} else if (initialExecution.status !== WorkflowExecutionStatus.RUNNING) {
|
||||||
|
signale.info(`[WORKFLOW] Execution ${executionId} already completed or cancelled with status ${initialExecution.status}, skipping`);
|
||||||
return; // Already completed or cancelled
|
return; // Already completed or cancelled
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if project is disabled
|
// Check if project is disabled
|
||||||
if (execution.workflow.project.disabled) {
|
if (initialExecution.workflow.project.disabled) {
|
||||||
console.warn(
|
signale.warn(
|
||||||
`[WORKFLOW] Project ${execution.workflow.projectId} (${execution.workflow.project.name}) is disabled, cancelling workflow execution ${executionId}`,
|
`[WORKFLOW] Project ${initialExecution.workflow.projectId} (${initialExecution.workflow.project.name}) is disabled, cancelling workflow execution ${executionId}`,
|
||||||
);
|
);
|
||||||
await prisma.workflowExecution.update({
|
await prisma.workflowExecution.update({
|
||||||
where: {id: executionId},
|
where: {id: executionId},
|
||||||
@@ -92,18 +103,70 @@ export class WorkflowExecutionService {
|
|||||||
// Note: We allow running executions to complete even if workflow is disabled
|
// Note: We allow running executions to complete even if workflow is disabled
|
||||||
// This prevents disruption to contacts who are already in the workflow
|
// This prevents disruption to contacts who are already in the workflow
|
||||||
// Only NEW executions are prevented when workflow is disabled (see startExecution in WorkflowService)
|
// Only NEW executions are prevented when workflow is disabled (see startExecution in WorkflowService)
|
||||||
if (!execution.workflow.enabled) {
|
if (!initialExecution.workflow.enabled) {
|
||||||
console.info(
|
signale.info(
|
||||||
`[WORKFLOW] Workflow ${execution.workflow.id} (${execution.workflow.name}) is disabled, but allowing execution ${executionId} to continue`,
|
`[WORKFLOW] Workflow ${initialExecution.workflow.id} (${initialExecution.workflow.name}) is disabled, but allowing execution ${executionId} to continue`,
|
||||||
);
|
);
|
||||||
// Allow execution to continue - no action needed
|
// Allow execution to continue - no action needed
|
||||||
}
|
}
|
||||||
|
|
||||||
const step = execution.workflow.steps.find(s => s.id === stepId);
|
const step = initialExecution.workflow.steps.find(s => s.id === stepId);
|
||||||
if (!step) {
|
if (!step) {
|
||||||
|
signale.error(`[WORKFLOW] Step ${stepId} not found in workflow ${initialExecution.workflow.id}`);
|
||||||
throw new HttpException(404, 'Step not found in workflow');
|
throw new HttpException(404, 'Step not found in workflow');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signale.info(`[WORKFLOW] Found step: ${step.name} (${step.type})`);
|
||||||
|
|
||||||
|
// Track if this execution was in WAITING state (meaning it's a delayed step)
|
||||||
|
const isResumingFromDelay = initialExecution.status === WorkflowExecutionStatus.WAITING;
|
||||||
|
|
||||||
|
// If workflow execution is WAITING (e.g., from a delay), set it back to RUNNING
|
||||||
|
let execution = initialExecution;
|
||||||
|
if (initialExecution.status === WorkflowExecutionStatus.WAITING) {
|
||||||
|
signale.info(`[WORKFLOW] Setting execution ${executionId} from WAITING to RUNNING`);
|
||||||
|
await prisma.workflowExecution.update({
|
||||||
|
where: {id: executionId},
|
||||||
|
data: {
|
||||||
|
status: WorkflowExecutionStatus.RUNNING,
|
||||||
|
currentStepId: stepId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-fetch the execution with the updated status
|
||||||
|
const updatedExecution = await prisma.workflowExecution.findUnique({
|
||||||
|
where: {id: executionId},
|
||||||
|
include: {
|
||||||
|
workflow: {
|
||||||
|
include: {
|
||||||
|
steps: {
|
||||||
|
include: {
|
||||||
|
template: true,
|
||||||
|
outgoingTransitions: {
|
||||||
|
orderBy: {priority: 'asc'},
|
||||||
|
include: {toStep: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
project: {
|
||||||
|
select: {disabled: true, id: true, name: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contact: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!updatedExecution) {
|
||||||
|
signale.error(`[WORKFLOW] Execution ${executionId} not found after status update`);
|
||||||
|
throw new HttpException(404, 'Workflow execution not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
execution = updatedExecution;
|
||||||
|
|
||||||
|
signale.info(`[WORKFLOW] Execution ${executionId} status updated to RUNNING`);
|
||||||
|
}
|
||||||
|
|
||||||
// Create or get step execution record
|
// Create or get step execution record
|
||||||
let stepExecution = await prisma.workflowStepExecution.findFirst({
|
let stepExecution = await prisma.workflowStepExecution.findFirst({
|
||||||
where: {
|
where: {
|
||||||
@@ -114,6 +177,8 @@ export class WorkflowExecutionService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!stepExecution) {
|
if (!stepExecution) {
|
||||||
|
signale.info(`[WORKFLOW] Creating new step execution for ${executionId} -> ${stepId}`);
|
||||||
|
try {
|
||||||
stepExecution = await prisma.workflowStepExecution.create({
|
stepExecution = await prisma.workflowStepExecution.create({
|
||||||
data: {
|
data: {
|
||||||
executionId,
|
executionId,
|
||||||
@@ -122,6 +187,11 @@ export class WorkflowExecutionService {
|
|||||||
startedAt: new Date(),
|
startedAt: new Date(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
signale.info(`[WORKFLOW] Created step execution ${stepExecution.id}`);
|
||||||
|
} catch (error) {
|
||||||
|
signale.error(`[WORKFLOW] Failed to create step execution for ${executionId} -> ${stepId}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
stepExecution = await prisma.workflowStepExecution.update({
|
stepExecution = await prisma.workflowStepExecution.update({
|
||||||
where: {id: stepExecution.id},
|
where: {id: stepExecution.id},
|
||||||
@@ -130,11 +200,14 @@ export class WorkflowExecutionService {
|
|||||||
startedAt: stepExecution.startedAt || new Date(),
|
startedAt: stepExecution.startedAt || new Date(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
signale.info(`[WORKFLOW] Updated existing step execution ${stepExecution.id} to RUNNING`);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Execute the step based on its type
|
// Execute the step based on its type
|
||||||
|
signale.info(`[WORKFLOW] Executing step ${stepId} of type ${step.type}`);
|
||||||
const result = await this.executeStep(step, execution, stepExecution);
|
const result = await this.executeStep(step, execution, stepExecution);
|
||||||
|
signale.info(`[WORKFLOW] Step ${stepId} executed successfully`);
|
||||||
|
|
||||||
// Check if step is in a waiting state (WAIT_FOR_EVENT steps only)
|
// Check if step is in a waiting state (WAIT_FOR_EVENT steps only)
|
||||||
// DELAY steps now mark themselves as COMPLETED and queue the next step
|
// DELAY steps now mark themselves as COMPLETED and queue the next step
|
||||||
@@ -148,7 +221,9 @@ export class WorkflowExecutionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if the workflow execution is now in WAITING state (DELAY steps do this)
|
// Check if the workflow execution is now in WAITING state (DELAY steps do this)
|
||||||
// If so, the step has already been handled and queued - don't process next steps
|
// Only check this if we're NOT resuming from a delay - if we are resuming,
|
||||||
|
// we've already set it to RUNNING and the step just executed
|
||||||
|
if (!isResumingFromDelay) {
|
||||||
const updatedExecution = await prisma.workflowExecution.findUnique({
|
const updatedExecution = await prisma.workflowExecution.findUnique({
|
||||||
where: {id: execution.id},
|
where: {id: execution.id},
|
||||||
});
|
});
|
||||||
@@ -157,6 +232,7 @@ export class WorkflowExecutionService {
|
|||||||
// Workflow is waiting (DELAY step has queued the next step) - don't process next steps now
|
// Workflow is waiting (DELAY step has queued the next step) - don't process next steps now
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Mark step as completed (for normal steps that complete immediately)
|
// Mark step as completed (for normal steps that complete immediately)
|
||||||
await prisma.workflowStepExecution.update({
|
await prisma.workflowStepExecution.update({
|
||||||
@@ -171,7 +247,7 @@ export class WorkflowExecutionService {
|
|||||||
// Determine next step(s) based on transitions and conditions
|
// Determine next step(s) based on transitions and conditions
|
||||||
await this.processNextSteps(execution, step, result);
|
await this.processNextSteps(execution, step, result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[WORKFLOW] Error executing step ${step.id}:`, error);
|
signale.error(`[WORKFLOW] Error executing step ${step.id}:`, error);
|
||||||
// Mark step as failed
|
// Mark step as failed
|
||||||
await prisma.workflowStepExecution.update({
|
await prisma.workflowStepExecution.update({
|
||||||
where: {id: stepExecution.id},
|
where: {id: stepExecution.id},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {useState} from 'react';
|
import {useState} from 'react';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -13,7 +14,6 @@ import {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
Badge,
|
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from '@plunk/ui';
|
} from '@plunk/ui';
|
||||||
import {Trash2, AlertCircle, Loader2} from 'lucide-react';
|
import {AlertCircle, Loader2, Trash2} from 'lucide-react';
|
||||||
import {toast} from 'sonner';
|
import {toast} from 'sonner';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import {useActiveProject} from '../lib/contexts/ActiveProjectProvider';
|
import {useActiveProject} from '../lib/contexts/ActiveProjectProvider';
|
||||||
@@ -76,13 +76,12 @@ export function DataManagementSettings() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Filter out standard fields and only show custom data fields
|
// Filter out standard fields and only show custom data fields
|
||||||
const customFields =
|
const customFields = fieldsData?.fields.filter(f => f.field.startsWith('data.')) || [];
|
||||||
fieldsData?.fields.filter(f => f.field.startsWith('data.')) || [];
|
|
||||||
|
|
||||||
// Filter out system events
|
// Filter out system events
|
||||||
const customEvents =
|
const customEvents =
|
||||||
eventsData?.eventNames.filter(
|
eventsData?.eventNames.filter(
|
||||||
name => !name.startsWith('email.') && !name.startsWith('segment.'),
|
name => !name.startsWith('email.') && !name.startsWith('segment.') && !name.startsWith('contact.'),
|
||||||
) || [];
|
) || [];
|
||||||
|
|
||||||
const handleDeleteField = async () => {
|
const handleDeleteField = async () => {
|
||||||
@@ -146,8 +145,8 @@ export function DataManagementSettings() {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Custom Contact Fields</CardTitle>
|
<CardTitle>Custom Contact Fields</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Manage custom fields stored in your contact data. You can only delete fields that are not
|
Manage custom fields stored in your contact data. You can only delete fields that are not used in any
|
||||||
used in any segments or campaigns.
|
segments or campaigns.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -166,9 +165,7 @@ export function DataManagementSettings() {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{customFields.map(field => (
|
{customFields.map(field => (
|
||||||
<TableRow key={field.field}>
|
<TableRow key={field.field}>
|
||||||
<TableCell className="font-mono text-sm">
|
<TableCell className="font-mono text-sm">{field.field.replace('data.', '')}</TableCell>
|
||||||
{field.field.replace('data.', '')}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant="secondary">{field.type}</Badge>
|
<Badge variant="secondary">{field.type}</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -176,11 +173,7 @@ export function DataManagementSettings() {
|
|||||||
<span className="text-sm text-muted-foreground">{field.coverage}%</span>
|
<span className="text-sm text-muted-foreground">{field.coverage}%</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<Button
|
<Button variant="ghost" size="sm" onClick={() => openFieldDeleteDialog(field.field)}>
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => openFieldDeleteDialog(field.field)}
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -197,8 +190,8 @@ export function DataManagementSettings() {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Custom Events</CardTitle>
|
<CardTitle>Custom Events</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Manage custom events tracked in your project. You can only delete events that are not used
|
Manage custom events tracked in your project. You can only delete events that are not used in any segments
|
||||||
in any segments or workflows. System events (email.*, segment.*) cannot be deleted.
|
or workflows. System events (email.*, segment.*) cannot be deleted.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -217,11 +210,7 @@ export function DataManagementSettings() {
|
|||||||
<TableRow key={eventName}>
|
<TableRow key={eventName}>
|
||||||
<TableCell className="font-mono text-sm">{eventName}</TableCell>
|
<TableCell className="font-mono text-sm">{eventName}</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<Button
|
<Button variant="ghost" size="sm" onClick={() => openEventDeleteDialog(eventName)}>
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => openEventDeleteDialog(eventName)}
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -237,9 +226,7 @@ export function DataManagementSettings() {
|
|||||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>{selectedField ? 'Delete Field' : 'Delete Event'}</DialogTitle>
|
||||||
{selectedField ? 'Delete Field' : 'Delete Event'}
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{selectedField
|
{selectedField
|
||||||
? `Are you sure you want to delete the field "${selectedField.replace('data.', '')}"?`
|
? `Are you sure you want to delete the field "${selectedField.replace('data.', '')}"?`
|
||||||
@@ -266,9 +253,7 @@ export function DataManagementSettings() {
|
|||||||
<AlertCircle className="h-4 w-4" />
|
<AlertCircle className="h-4 w-4" />
|
||||||
<div className="ml-2">
|
<div className="ml-2">
|
||||||
<p className="text-sm font-medium">Cannot delete this field</p>
|
<p className="text-sm font-medium">Cannot delete this field</p>
|
||||||
<p className="text-sm">
|
<p className="text-sm">This field is currently used in:</p>
|
||||||
This field is currently used in:
|
|
||||||
</p>
|
|
||||||
{fieldUsage.usedInSegments.length > 0 && (
|
{fieldUsage.usedInSegments.length > 0 && (
|
||||||
<ul className="mt-2 list-disc list-inside text-sm">
|
<ul className="mt-2 list-disc list-inside text-sm">
|
||||||
{fieldUsage.usedInSegments.map(segment => (
|
{fieldUsage.usedInSegments.map(segment => (
|
||||||
@@ -311,9 +296,7 @@ export function DataManagementSettings() {
|
|||||||
<AlertCircle className="h-4 w-4" />
|
<AlertCircle className="h-4 w-4" />
|
||||||
<div className="ml-2">
|
<div className="ml-2">
|
||||||
<p className="text-sm font-medium">Cannot delete this event</p>
|
<p className="text-sm font-medium">Cannot delete this event</p>
|
||||||
<p className="text-sm">
|
<p className="text-sm">This event is currently used in:</p>
|
||||||
This event is currently used in:
|
|
||||||
</p>
|
|
||||||
{eventUsage.usedInSegments.length > 0 && (
|
{eventUsage.usedInSegments.length > 0 && (
|
||||||
<ul className="mt-2 list-disc list-inside text-sm">
|
<ul className="mt-2 list-disc list-inside text-sm">
|
||||||
{eventUsage.usedInSegments.map(segment => (
|
{eventUsage.usedInSegments.map(segment => (
|
||||||
@@ -350,9 +333,7 @@ export function DataManagementSettings() {
|
|||||||
variant="destructive"
|
variant="destructive"
|
||||||
onClick={selectedField ? handleDeleteField : handleDeleteEvent}
|
onClick={selectedField ? handleDeleteField : handleDeleteEvent}
|
||||||
disabled={
|
disabled={
|
||||||
isDeleting ||
|
isDeleting || (!!selectedField && !fieldUsage?.canDelete) || (!!selectedEvent && !eventUsage?.canDelete)
|
||||||
(!!selectedField && !fieldUsage?.canDelete) ||
|
|
||||||
(!!selectedEvent && !eventUsage?.canDelete)
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{isDeleting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
{isDeleting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||||
|
|||||||
Reference in New Issue
Block a user