Update all triggers to display standard cards for existing triggers with delete buttons

This commit is contained in:
akhisud3195 2025-08-19 13:03:39 +05:30
parent d592354080
commit e448601046
4 changed files with 381 additions and 153 deletions

View file

@ -21,13 +21,13 @@ export function JobRulesTabs({ projectId }: { projectId: string }) {
aria-label="Job Rules" aria-label="Job Rules"
fullWidth fullWidth
> >
<Tab key="triggers" title="Triggers"> <Tab key="triggers" title="External Triggers">
<TriggersTab projectId={projectId} /> <TriggersTab projectId={projectId} />
</Tab> </Tab>
<Tab key="scheduled" title="One-time"> <Tab key="scheduled" title="One-Time Triggers">
<ScheduledJobRulesList projectId={projectId} /> <ScheduledJobRulesList projectId={projectId} />
</Tab> </Tab>
<Tab key="recurring" title="Recurring"> <Tab key="recurring" title="Recurring Triggers">
<RecurringJobRulesList projectId={projectId} /> <RecurringJobRulesList projectId={projectId} />
</Tab> </Tab>
</Tabs> </Tabs>

View file

@ -4,11 +4,11 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { Link, Spinner } from "@heroui/react"; import { Link, Spinner } from "@heroui/react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Panel } from "@/components/common/panel-common"; import { Panel } from "@/components/common/panel-common";
import { listRecurringJobRules } from "@/app/actions/recurring-job-rules.actions"; import { listRecurringJobRules, deleteRecurringJobRule } from "@/app/actions/recurring-job-rules.actions";
import { z } from "zod"; import { z } from "zod";
import { ListedRecurringRuleItem } from "@/src/application/repositories/recurring-job-rules.repository.interface"; import { ListedRecurringRuleItem } from "@/src/application/repositories/recurring-job-rules.repository.interface";
import { isToday, isThisWeek, isThisMonth } from "@/lib/utils/date"; import { isToday, isThisWeek, isThisMonth } from "@/lib/utils/date";
import { PlusIcon } from "lucide-react"; import { PlusIcon, Trash2 } from "lucide-react";
type ListedItem = z.infer<typeof ListedRecurringRuleItem>; type ListedItem = z.infer<typeof ListedRecurringRuleItem>;
@ -18,6 +18,7 @@ export function RecurringJobRulesList({ projectId }: { projectId: string }) {
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
const [loadingMore, setLoadingMore] = useState<boolean>(false); const [loadingMore, setLoadingMore] = useState<boolean>(false);
const [hasMore, setHasMore] = useState<boolean>(false); const [hasMore, setHasMore] = useState<boolean>(false);
const [deletingRule, setDeletingRule] = useState<string | null>(null);
const fetchPage = useCallback(async (cursorArg?: string | null) => { const fetchPage = useCallback(async (cursorArg?: string | null) => {
const res = await listRecurringJobRules({ projectId, cursor: cursorArg ?? undefined, limit: 20 }); const res = await listRecurringJobRules({ projectId, cursor: cursorArg ?? undefined, limit: 20 });
@ -48,6 +49,24 @@ export function RecurringJobRulesList({ projectId }: { projectId: string }) {
setLoadingMore(false); setLoadingMore(false);
}, [cursor, fetchPage]); }, [cursor, fetchPage]);
const handleDeleteRule = async (ruleId: string) => {
if (!window.confirm('Are you sure you want to delete this recurring trigger?')) {
return;
}
try {
setDeletingRule(ruleId);
await deleteRecurringJobRule({ projectId, ruleId });
// Remove the deleted item from the list
setItems(prev => prev.filter(item => item.id !== ruleId));
} catch (err: any) {
console.error('Error deleting recurring trigger:', err);
alert('Failed to delete recurring trigger. Please try again.');
} finally {
setDeletingRule(null);
}
};
const sections = useMemo(() => { const sections = useMemo(() => {
const groups: Record<string, ListedItem[]> = { const groups: Record<string, ListedItem[]> = {
Today: [], Today: [],
@ -109,18 +128,15 @@ export function RecurringJobRulesList({ projectId }: { projectId: string }) {
return ( return (
<Panel <Panel
title={ title={
<div className="flex items-center gap-3"> <div className="text-base font-normal text-gray-900 dark:text-gray-100">
<div className="text-sm font-medium text-gray-900 dark:text-gray-100"> Run your assistant workflow on an automated repeating schedule (cron jobs).
RECURRING JOB RULES
</div>
</div> </div>
} }
rightActions={ rightActions={
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href={`/projects/${projectId}/job-rules/recurring/new`}> <Link href={`/projects/${projectId}/job-rules/recurring/new`}>
<Button size="sm" className="flex items-center gap-2"> <Button size="sm" className="whitespace-nowrap" startContent={<PlusIcon className="w-4 h-4" />}>
<PlusIcon className="w-4 h-4" /> New Recurring Trigger
New Rule
</Button> </Button>
</Link> </Link>
</div> </div>
@ -145,38 +161,48 @@ export function RecurringJobRulesList({ projectId }: { projectId: string }) {
</h3> </h3>
<div className="grid gap-3"> <div className="grid gap-3">
{sectionItems.map((item) => ( {sectionItems.map((item) => (
<Link <div
key={item.id} key={item.id}
href={`/projects/${projectId}/job-rules/recurring/${item.id}`}
className="block p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 transition-colors" className="block p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 transition-colors"
> >
<div className="flex items-center justify-between"> <div className="flex items-start justify-between">
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-3 mb-2"> <Link
<span className={`text-sm font-medium ${getStatusColor(item.disabled, item.lastError || null)}`}> href={`/projects/${projectId}/job-rules/recurring/${item.id}`}
{getStatusText(item.disabled, item.lastError || null)} className="block"
</span> >
<span className="text-sm text-gray-500 dark:text-gray-400"> <div className="flex items-center gap-3 mb-2">
Next run: {formatNextRunAt(item.nextRunAt)} <span className={`text-sm font-medium ${getStatusColor(item.disabled, item.lastError || null)}`}>
</span> {getStatusText(item.disabled, item.lastError || null)}
</div> </span>
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1"> <span className="text-sm text-gray-500 dark:text-gray-400">
Schedule: {formatCronExpression(item.cron)} Next run: {formatNextRunAt(item.nextRunAt)}
</div> </span>
<div className="text-sm text-gray-600 dark:text-gray-400">
Created: {new Date(item.createdAt).toLocaleDateString()}
</div>
{item.lastError && (
<div className="text-sm text-red-600 dark:text-red-400 mt-1">
Last error: {item.lastError}
</div> </div>
)} <div className="text-sm text-gray-600 dark:text-gray-400 mb-1">
</div> Schedule: {formatCronExpression(item.cron)}
<div className="text-sm text-gray-500 dark:text-gray-400"> </div>
{new Date(item.createdAt).toLocaleDateString()} <div className="text-sm text-gray-600 dark:text-gray-400">
Created: {new Date(item.createdAt).toLocaleDateString()}
</div>
{item.lastError && (
<div className="text-sm text-red-600 dark:text-red-400 mt-1">
Last error: {item.lastError}
</div>
)}
</Link>
</div> </div>
<Button
variant="tertiary"
size="sm"
isLoading={deletingRule === item.id}
onClick={() => handleDeleteRule(item.id)}
className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:text-red-400 dark:hover:text-red-300 dark:hover:bg-red-950"
>
<Trash2 className="w-4 h-4" />
</Button>
</div> </div>
</Link> </div>
))} ))}
</div> </div>
</div> </div>
@ -184,7 +210,7 @@ export function RecurringJobRulesList({ projectId }: { projectId: string }) {
})} })}
{items.length === 0 && !loading && ( {items.length === 0 && !loading && (
<div className="text-center py-8 text-gray-500 dark:text-gray-400"> <div className="text-center py-8 text-gray-500 dark:text-gray-400">
No recurring job rules found. Create your first rule to get started. No recurring triggers yet. Create your first recurring trigger to get started.
</div> </div>
)} )}
{hasMore && ( {hasMore && (

View file

@ -1,12 +1,15 @@
'use client'; 'use client';
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { Button, Spinner, Card, CardBody, CardHeader } from '@heroui/react'; import { Spinner } from '@heroui/react';
import { Plus, Trash2, ZapIcon } from 'lucide-react'; import { Button } from '@/components/ui/button';
import { Panel } from '@/components/common/panel-common';
import { Plus, Trash2, ZapIcon, ChevronDown, ChevronUp } from 'lucide-react';
import { z } from 'zod'; import { z } from 'zod';
import { ComposioTriggerDeployment } from '@/src/entities/models/composio-trigger-deployment'; import { ComposioTriggerDeployment } from '@/src/entities/models/composio-trigger-deployment';
import { ComposioTriggerType } from '@/src/entities/models/composio-trigger-type'; import { ComposioTriggerType } from '@/src/entities/models/composio-trigger-type';
import { listComposioTriggerDeployments, deleteComposioTriggerDeployment, createComposioTriggerDeployment } from '@/app/actions/composio.actions'; import { isToday, isThisWeek, isThisMonth } from '@/lib/utils/date';
import { listComposioTriggerDeployments, deleteComposioTriggerDeployment, createComposioTriggerDeployment, listComposioTriggerTypes } from '@/app/actions/composio.actions';
import { SelectComposioToolkit } from '../../tools/components/SelectComposioToolkit'; import { SelectComposioToolkit } from '../../tools/components/SelectComposioToolkit';
import { ComposioTriggerTypesPanel } from '../../workflow/components/ComposioTriggerTypesPanel'; import { ComposioTriggerTypesPanel } from '../../workflow/components/ComposioTriggerTypesPanel';
import { TriggerConfigForm } from '../../workflow/components/TriggerConfigForm'; import { TriggerConfigForm } from '../../workflow/components/TriggerConfigForm';
@ -28,6 +31,11 @@ export function TriggersTab({ projectId }: { projectId: string }) {
const [isSubmittingTrigger, setIsSubmittingTrigger] = useState(false); const [isSubmittingTrigger, setIsSubmittingTrigger] = useState(false);
const [deletingTrigger, setDeletingTrigger] = useState<string | null>(null); const [deletingTrigger, setDeletingTrigger] = useState<string | null>(null);
const [projectConfig, setProjectConfig] = useState<z.infer<typeof Project> | null>(null); const [projectConfig, setProjectConfig] = useState<z.infer<typeof Project> | null>(null);
const [triggerTypeNames, setTriggerTypeNames] = useState<Record<string, string>>({});
const [expandedTrigger, setExpandedTrigger] = useState<string | null>(null);
const [cursor, setCursor] = useState<string | null>(null);
const [hasMore, setHasMore] = useState<boolean>(false);
const [loadingMore, setLoadingMore] = useState<boolean>(false);
const loadProjectConfig = useCallback(async () => { const loadProjectConfig = useCallback(async () => {
try { try {
@ -38,12 +46,56 @@ export function TriggersTab({ projectId }: { projectId: string }) {
} }
}, [projectId]); }, [projectId]);
const loadTriggerTypeNames = useCallback(async () => {
try {
const names: Record<string, string> = {};
// Get unique toolkit slugs from existing triggers
const uniqueToolkits = [...new Set(triggers.map(t => t.toolkitSlug))];
// Fetch trigger types for each toolkit
for (const toolkitSlug of uniqueToolkits) {
try {
const response = await listComposioTriggerTypes(toolkitSlug);
response.items.forEach(triggerType => {
names[triggerType.slug] = triggerType.name;
});
} catch (err) {
console.error(`Error fetching trigger types for ${toolkitSlug}:`, err);
}
}
setTriggerTypeNames(names);
} catch (err: any) {
console.error('Error loading trigger type names:', err);
}
}, [triggers]);
const sections = useMemo(() => {
const groups: Record<string, TriggerDeployment[]> = {
Today: [],
'This week': [],
'This month': [],
Older: [],
};
for (const trigger of triggers) {
const d = new Date(trigger.createdAt);
if (isToday(d)) groups['Today'].push(trigger);
else if (isThisWeek(d)) groups['This week'].push(trigger);
else if (isThisMonth(d)) groups['This month'].push(trigger);
else groups['Older'].push(trigger);
}
return groups;
}, [triggers]);
const loadTriggers = useCallback(async () => { const loadTriggers = useCallback(async () => {
try { try {
setLoading(true); setLoading(true);
setError(null); setError(null);
const response = await listComposioTriggerDeployments({ projectId }); const response = await listComposioTriggerDeployments({ projectId });
setTriggers(response.items); setTriggers(response.items);
setCursor(response.nextCursor);
setHasMore(Boolean(response.nextCursor));
} catch (err: any) { } catch (err: any) {
console.error('Error loading triggers:', err); console.error('Error loading triggers:', err);
setError('Failed to load triggers. Please try again.'); setError('Failed to load triggers. Please try again.');
@ -52,6 +104,21 @@ export function TriggersTab({ projectId }: { projectId: string }) {
} }
}, [projectId]); }, [projectId]);
const loadMore = useCallback(async () => {
if (!cursor) return;
setLoadingMore(true);
try {
const response = await listComposioTriggerDeployments({ projectId, cursor });
setTriggers(prev => [...prev, ...response.items]);
setCursor(response.nextCursor);
setHasMore(Boolean(response.nextCursor));
} catch (err: any) {
console.error('Error loading more triggers:', err);
} finally {
setLoadingMore(false);
}
}, [cursor, projectId]);
const handleDeleteTrigger = async (deploymentId: string) => { const handleDeleteTrigger = async (deploymentId: string) => {
if (!window.confirm('Are you sure you want to delete this trigger?')) { if (!window.confirm('Are you sure you want to delete this trigger?')) {
return; return;
@ -79,6 +146,7 @@ export function TriggersTab({ projectId }: { projectId: string }) {
setSelectedTriggerType(null); setSelectedTriggerType(null);
setShowAuthModal(false); setShowAuthModal(false);
setIsSubmittingTrigger(false); setIsSubmittingTrigger(false);
setExpandedTrigger(null); // Reset expanded state
loadTriggers(); // Reload in case any triggers were created loadTriggers(); // Reload in case any triggers were created
}; };
@ -157,106 +225,217 @@ export function TriggersTab({ projectId }: { projectId: string }) {
} }
}, [showCreateFlow, loadTriggers]); }, [showCreateFlow, loadTriggers]);
useEffect(() => {
if (triggers.length > 0) {
loadTriggerTypeNames();
}
}, [triggers, loadTriggerTypeNames]);
const renderTriggerList = () => { const renderTriggerList = () => {
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center py-8"> <Panel
<Spinner size="lg" /> title={
<span className="ml-2">Loading triggers...</span> <div className="text-base font-normal text-gray-900 dark:text-gray-100">
</div> Loading your triggers
</div>
}
>
<div className="h-full overflow-auto px-4 py-4">
<div className="max-w-[1024px] mx-auto">
<div className="flex items-center justify-center py-8">
<Spinner size="lg" />
<span className="ml-2">Loading triggers...</span>
</div>
</div>
</div>
</Panel>
); );
} }
if (error) { if (error) {
return ( return (
<div className="text-center py-8"> <Panel
<p className="text-red-500 mb-4">{error}</p> title={
<Button variant="flat" onPress={loadTriggers}> <div className="text-base font-normal text-gray-900 dark:text-gray-100">
Try Again Error loading your triggers
</Button> </div>
</div> }
rightActions={
<Button variant="secondary" onClick={loadTriggers}>
Try Again
</Button>
}
>
<div className="h-full overflow-auto px-4 py-4">
<div className="max-w-[1024px] mx-auto">
<div className="text-center py-8">
<p className="text-red-500 mb-4">{error}</p>
</div>
</div>
</div>
</Panel>
); );
} }
if (triggers.length === 0) { if (triggers.length === 0) {
return ( return (
<div className="text-center py-12"> <Panel
<ZapIcon className="w-16 h-16 mx-auto text-gray-400 mb-4" /> title={
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2"> <div className="text-base font-normal text-gray-900 dark:text-gray-100">
No triggers configured Listen for events from connected apps to run your assistant workflow automatically.
</h3> </div>
<p className="text-gray-500 dark:text-gray-400 mb-6"> }
Set up your first trigger to listen for events from your connected apps. rightActions={
</p> <Button
<Button variant="primary"
color="primary" startContent={<Plus className="w-4 h-4" />}
variant="solid" onClick={handleCreateNew}
startContent={<Plus className="w-4 h-4" />} className="whitespace-nowrap"
onPress={handleCreateNew} >
> New External Trigger
Create your first trigger </Button>
</Button> }
</div> >
<div className="h-full overflow-auto px-4 py-4">
<div className="max-w-[1024px] mx-auto">
<div className="text-center py-12">
<ZapIcon className="w-16 h-16 mx-auto text-gray-400 mb-4" />
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
No external triggers yet
</h3>
<p className="text-gray-500 dark:text-gray-400 mb-6">
Create your first external trigger to listen for events from your connected apps.
</p>
</div>
</div>
</div>
</Panel>
); );
} }
return ( return (
<div className="space-y-4"> <Panel
<div className="flex justify-between items-center"> title={
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100"> <div className="text-base font-normal text-gray-900 dark:text-gray-100">
Active Triggers ({triggers.length}) Listen for events from connected apps to run your assistant workflow automatically.
</h3> </div>
}
rightActions={
<Button <Button
color="primary" variant="primary"
variant="solid"
startContent={<Plus className="w-4 h-4" />} startContent={<Plus className="w-4 h-4" />}
onPress={handleCreateNew} onClick={handleCreateNew}
className="whitespace-nowrap"
> >
Create New Trigger New External Trigger
</Button> </Button>
</div> }
>
<div className="space-y-3"> <div className="h-full overflow-auto px-4 py-4">
{triggers.map((trigger) => ( <div className="max-w-[1024px] mx-auto">
<Card key={trigger.id} className="w-full"> <div className="flex flex-col gap-6">
<CardHeader className="flex justify-between items-start"> {Object.entries(sections).map(([sectionName, sectionTriggers]) => {
<div> if (sectionTriggers.length === 0) return null;
<h4 className="text-base font-medium text-gray-900 dark:text-gray-100"> return (
{trigger.triggerTypeSlug} <div key={sectionName} className="space-y-3">
</h4> <h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
<p className="text-sm text-gray-500 dark:text-gray-400"> {sectionName}
Created {new Date(trigger.createdAt).toLocaleDateString()} </h3>
</p> <div className="grid gap-3">
</div> {sectionTriggers.map((trigger) => (
<Button <div
isIconOnly key={trigger.id}
variant="light" className="block p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 transition-colors"
color="danger" >
size="sm" <div className="flex items-start justify-between">
isLoading={deletingTrigger === trigger.id} <div className="flex-1">
onPress={() => handleDeleteTrigger(trigger.id)} <div className="flex items-center gap-3 mb-2">
> <span className="text-sm font-medium text-green-600 dark:text-green-400">
<Trash2 className="w-4 h-4" /> Active
</Button> </span>
</CardHeader> <span className="text-sm font-medium text-gray-900 dark:text-gray-100">
<CardBody className="pt-0"> {triggerTypeNames[trigger.triggerTypeSlug] || trigger.triggerTypeSlug}
<div className="text-sm text-gray-600 dark:text-gray-300"> </span>
<p><strong>Trigger ID:</strong> {trigger.triggerId}</p> </div>
<p><strong>Connected Account:</strong> {trigger.connectedAccountId}</p> <div className="text-sm text-gray-500 dark:text-gray-400">
{Object.keys(trigger.triggerConfig).length > 0 && ( Created: {new Date(trigger.createdAt).toLocaleDateString()}
<div className="mt-2"> </div>
<strong>Configuration:</strong> {Object.keys(trigger.triggerConfig).length > 0 && (
<pre className="mt-1 text-xs bg-gray-100 dark:bg-gray-800 p-2 rounded"> <div className="text-sm text-gray-600 dark:text-gray-400 mt-1">
{JSON.stringify(trigger.triggerConfig, null, 2)} Configuration: {Object.keys(trigger.triggerConfig).length} settings
</pre> </div>
)}
</div>
<Button
variant="tertiary"
size="sm"
isLoading={deletingTrigger === trigger.id}
onClick={() => handleDeleteTrigger(trigger.id)}
className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:text-red-400 dark:hover:text-red-300 dark:hover:bg-red-950"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
{/* Advanced Details Section - Collapsible */}
<div className="mt-3">
<button
onClick={() => setExpandedTrigger(expandedTrigger === trigger.id ? null : trigger.id)}
className="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
>
<span className="font-medium">Advanced Details</span>
{expandedTrigger === trigger.id ? (
<ChevronUp className="w-3 h-3" />
) : (
<ChevronDown className="w-3 h-3" />
)}
</button>
{expandedTrigger === trigger.id && (
<div className="mt-2 space-y-1">
<div className="text-xs text-gray-600 dark:text-gray-400">
<span className="font-medium">Slug:</span> {trigger.triggerTypeSlug}
</div>
<div className="text-xs text-gray-600 dark:text-gray-400">
<span className="font-medium">Trigger ID:</span> {trigger.triggerId}
</div>
<div className="text-xs text-gray-600 dark:text-gray-400">
<span className="font-medium">Connected Account:</span> {trigger.connectedAccountId}
</div>
</div>
)}
</div>
</div>
))}
</div> </div>
)} </div>
);
})}
{hasMore && (
<div className="text-center">
<Button
onClick={loadMore}
disabled={loadingMore}
variant="secondary"
size="sm"
>
{loadingMore ? (
<>
<Spinner size="sm" />
Loading...
</>
) : (
'Load More'
)}
</Button>
</div> </div>
</CardBody> )}
</Card> </div>
))} </div>
</div> </div>
</div> </Panel>
); );
}; };
@ -288,8 +467,8 @@ export function TriggersTab({ projectId }: { projectId: string }) {
Select a Toolkit to Create Trigger Select a Toolkit to Create Trigger
</h3> </h3>
<Button <Button
variant="flat" variant="secondary"
onPress={handleBackToList} onClick={handleBackToList}
> >
Back to Triggers Back to Triggers
</Button> </Button>
@ -320,11 +499,7 @@ export function TriggersTab({ projectId }: { projectId: string }) {
return ( return (
<> <>
<div className="h-full overflow-auto px-4 py-4"> {showCreateFlow ? renderCreateFlow() : renderTriggerList()}
<div className="max-w-[1024px] mx-auto">
{showCreateFlow ? renderCreateFlow() : renderTriggerList()}
</div>
</div>
{/* Auth Modal */} {/* Auth Modal */}
{selectedToolkit && ( {selectedToolkit && (

View file

@ -4,11 +4,11 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { Link, Spinner } from "@heroui/react"; import { Link, Spinner } from "@heroui/react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Panel } from "@/components/common/panel-common"; import { Panel } from "@/components/common/panel-common";
import { listScheduledJobRules } from "@/app/actions/scheduled-job-rules.actions"; import { listScheduledJobRules, deleteScheduledJobRule } from "@/app/actions/scheduled-job-rules.actions";
import { z } from "zod"; import { z } from "zod";
import { ListedRuleItem } from "@/src/application/repositories/scheduled-job-rules.repository.interface"; import { ListedRuleItem } from "@/src/application/repositories/scheduled-job-rules.repository.interface";
import { isToday, isThisWeek, isThisMonth } from "@/lib/utils/date"; import { isToday, isThisWeek, isThisMonth } from "@/lib/utils/date";
import { PlusIcon } from "lucide-react"; import { PlusIcon, Trash2 } from "lucide-react";
type ListedItem = z.infer<typeof ListedRuleItem>; type ListedItem = z.infer<typeof ListedRuleItem>;
@ -18,6 +18,7 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) {
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
const [loadingMore, setLoadingMore] = useState<boolean>(false); const [loadingMore, setLoadingMore] = useState<boolean>(false);
const [hasMore, setHasMore] = useState<boolean>(false); const [hasMore, setHasMore] = useState<boolean>(false);
const [deletingRule, setDeletingRule] = useState<string | null>(null);
const fetchPage = useCallback(async (cursorArg?: string | null) => { const fetchPage = useCallback(async (cursorArg?: string | null) => {
const res = await listScheduledJobRules({ projectId, cursor: cursorArg ?? undefined, limit: 20 }); const res = await listScheduledJobRules({ projectId, cursor: cursorArg ?? undefined, limit: 20 });
@ -48,6 +49,24 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) {
setLoadingMore(false); setLoadingMore(false);
}, [cursor, fetchPage]); }, [cursor, fetchPage]);
const handleDeleteRule = async (ruleId: string) => {
if (!window.confirm('Are you sure you want to delete this one-time trigger?')) {
return;
}
try {
setDeletingRule(ruleId);
await deleteScheduledJobRule({ projectId, ruleId });
// Remove the deleted item from the list
setItems(prev => prev.filter(item => item.id !== ruleId));
} catch (err: any) {
console.error('Error deleting one-time trigger:', err);
alert('Failed to delete one-time trigger. Please try again.');
} finally {
setDeletingRule(null);
}
};
const sections = useMemo(() => { const sections = useMemo(() => {
const groups: Record<string, ListedItem[]> = { const groups: Record<string, ListedItem[]> = {
Today: [], Today: [],
@ -87,18 +106,15 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) {
return ( return (
<Panel <Panel
title={ title={
<div className="flex items-center gap-3"> <div className="text-base font-normal text-gray-900 dark:text-gray-100">
<div className="text-sm font-medium text-gray-900 dark:text-gray-100"> Schedule a single job to run your assistant workflow at a specific date and time.
SCHEDULED JOB RULES
</div>
</div> </div>
} }
rightActions={ rightActions={
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href={`/projects/${projectId}/job-rules/scheduled/new`}> <Link href={`/projects/${projectId}/job-rules/scheduled/new`}>
<Button size="sm" className="flex items-center gap-2"> <Button size="sm" className="whitespace-nowrap" startContent={<PlusIcon className="w-4 h-4" />}>
<PlusIcon className="w-4 h-4" /> New One-time Trigger
New Rule
</Button> </Button>
</Link> </Link>
</div> </div>
@ -123,30 +139,40 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) {
</h3> </h3>
<div className="grid gap-3"> <div className="grid gap-3">
{sectionItems.map((item) => ( {sectionItems.map((item) => (
<Link <div
key={item.id} key={item.id}
href={`/projects/${projectId}/job-rules/scheduled/${item.id}`}
className="block p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 transition-colors" className="block p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 transition-colors"
> >
<div className="flex items-center justify-between"> <div className="flex items-start justify-between">
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-3 mb-2"> <Link
<span className={`text-sm font-medium ${getStatusColor(item.status, item.processedAt || null)}`}> href={`/projects/${projectId}/job-rules/scheduled/${item.id}`}
{getStatusText(item.status, item.processedAt || null)} className="block"
</span> >
<span className="text-sm text-gray-500 dark:text-gray-400"> <div className="flex items-center gap-3 mb-2">
Next run: {formatNextRunAt(item.nextRunAt)} <span className={`text-sm font-medium ${getStatusColor(item.status, item.processedAt || null)}`}>
</span> {getStatusText(item.status, item.processedAt || null)}
</div> </span>
<div className="text-sm text-gray-600 dark:text-gray-400"> <span className="text-sm text-gray-500 dark:text-gray-400">
Created: {new Date(item.createdAt).toLocaleDateString()} Next run: {formatNextRunAt(item.nextRunAt)}
</div> </span>
</div> </div>
<div className="text-sm text-gray-500 dark:text-gray-400"> <div className="text-sm text-gray-600 dark:text-gray-400">
{new Date(item.createdAt).toLocaleDateString()} Created: {new Date(item.createdAt).toLocaleDateString()}
</div>
</Link>
</div> </div>
<Button
variant="tertiary"
size="sm"
isLoading={deletingRule === item.id}
onClick={() => handleDeleteRule(item.id)}
className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:text-red-400 dark:hover:text-red-300 dark:hover:bg-red-950"
>
<Trash2 className="w-4 h-4" />
</Button>
</div> </div>
</Link> </div>
))} ))}
</div> </div>
</div> </div>
@ -154,7 +180,7 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) {
})} })}
{items.length === 0 && !loading && ( {items.length === 0 && !loading && (
<div className="text-center py-8 text-gray-500 dark:text-gray-400"> <div className="text-center py-8 text-gray-500 dark:text-gray-400">
No scheduled job rules found. Create your first rule to get started. No one-time triggers yet. Create your first one-time trigger to get started.
</div> </div>
)} )}
{hasMore && ( {hasMore && (
@ -183,3 +209,4 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) {
</Panel> </Panel>
); );
} }