Triggers revamp (#212)

* Simplify composio trigger name to be computed directly from Slug

* Show log and friendly name in composio trigger cards

* Standardize buttons in all trigger creation flows

* Update trigger cards look

* Remove extra ring around fields in trigger config form

* Add copilot welcome message

* Update copilot welcome message

* Fix @ mentions deletion glitch
This commit is contained in:
Akhilesh Sudhakar 2025-08-19 21:35:03 +08:00 committed by GitHub
parent 82e8b6fadf
commit 9bee30aade
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 201 additions and 161 deletions

View file

@ -103,8 +103,7 @@ export function CreateRecurringJobRuleForm({ projectId }: { projectId: string })
title={
<div className="flex items-center gap-3">
<Link href={`/projects/${projectId}/job-rules`}>
<Button variant="secondary" size="sm">
<ArrowLeftIcon className="w-4 h-4 mr-2" />
<Button variant="secondary" size="sm" startContent={<ArrowLeftIcon className="w-4 h-4" />} className="whitespace-nowrap">
Back
</Button>
</Link>
@ -181,9 +180,9 @@ export function CreateRecurringJobRuleForm({ projectId }: { projectId: string })
onClick={addMessage}
variant="secondary"
size="sm"
className="flex items-center gap-2"
startContent={<PlusIcon className="w-4 h-4" />}
className="whitespace-nowrap"
>
<PlusIcon className="w-4 h-4" />
Add Message
</Button>
</div>
@ -231,7 +230,8 @@ export function CreateRecurringJobRuleForm({ projectId }: { projectId: string })
<Button
type="submit"
disabled={loading}
className="px-6 py-2"
isLoading={loading}
className="px-6 py-2 whitespace-nowrap"
>
{loading ? "Creating..." : "Create Rule"}
</Button>

View file

@ -134,8 +134,7 @@ export function RecurringJobRuleView({ projectId, ruleId }: { projectId: string;
title={
<div className="flex items-center gap-3">
<Link href={`/projects/${projectId}/job-rules`}>
<Button variant="secondary" size="sm">
<ArrowLeftIcon className="w-4 h-4 mr-2" />
<Button variant="secondary" size="sm" startContent={<ArrowLeftIcon className="w-4 h-4" />} className="whitespace-nowrap">
Back
</Button>
</Link>
@ -149,31 +148,21 @@ export function RecurringJobRuleView({ projectId, ruleId }: { projectId: string;
<Button
onClick={handleToggleStatus}
disabled={updating}
variant={rule.disabled ? "secondary" : "primary"}
variant={rule.disabled ? "primary" : "secondary"}
size="sm"
className="flex items-center gap-2"
isLoading={updating}
startContent={rule.disabled ? <PlayIcon className="w-4 h-4" /> : <PauseIcon className="w-4 h-4" />}
className="whitespace-nowrap"
>
{updating ? (
<Spinner size="sm" />
) : rule.disabled ? (
<>
<PlayIcon className="w-4 h-4" />
Enable
</>
) : (
<>
<PauseIcon className="w-4 h-4" />
Disable
</>
)}
{rule.disabled ? 'Activate' : 'Pause'}
</Button>
<Button
onClick={() => setShowDeleteConfirm(true)}
variant="secondary"
size="sm"
className="flex items-center gap-2 bg-red-50 hover:bg-red-100 text-red-700 dark:bg-red-950 dark:hover:bg-red-900 dark:text-red-400 border border-red-200 dark:border-red-800"
startContent={<Trash2Icon className="w-4 h-4" />}
className="bg-red-50 hover:bg-red-100 text-red-700 dark:bg-red-950 dark:hover:bg-red-900 dark:text-red-400 border border-red-200 dark:border-red-800 whitespace-nowrap"
>
<Trash2Icon className="w-4 h-4" />
Delete
</Button>
</div>
@ -297,6 +286,7 @@ export function RecurringJobRuleView({ projectId, ruleId }: { projectId: string;
variant="secondary"
onClick={() => setShowDeleteConfirm(false)}
disabled={deleting}
className="whitespace-nowrap"
>
Cancel
</Button>
@ -304,19 +294,11 @@ export function RecurringJobRuleView({ projectId, ruleId }: { projectId: string;
variant="secondary"
onClick={handleDelete}
disabled={deleting}
className="flex items-center gap-2 bg-red-50 hover:bg-red-100 text-red-700 dark:bg-red-950 dark:hover:bg-red-900 dark:text-red-400 border border-red-200 dark:border-red-800"
isLoading={deleting}
startContent={<Trash2Icon className="w-4 h-4" />}
className="bg-red-50 hover:bg-red-100 text-red-700 dark:bg-red-950 dark:hover:bg-red-900 dark:text-red-400 border border-red-200 dark:border-red-800 whitespace-nowrap"
>
{deleting ? (
<>
<Spinner size="sm" />
Deleting...
</>
) : (
<>
<Trash2Icon className="w-4 h-4" />
Delete
</>
)}
{deleting ? 'Deleting...' : 'Delete'}
</Button>
</div>
</div>

View file

@ -220,15 +220,10 @@ export function RecurringJobRulesList({ projectId }: { projectId: string }) {
disabled={loadingMore}
variant="secondary"
size="sm"
isLoading={loadingMore}
className="whitespace-nowrap"
>
{loadingMore ? (
<>
<Spinner size="sm" />
Loading...
</>
) : (
'Load More'
)}
{loadingMore ? 'Loading...' : 'Load More'}
</Button>
</div>
)}

View file

@ -4,12 +4,12 @@ import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { Spinner, Link } from '@heroui/react';
import { Button } from '@/components/ui/button';
import { Panel } from '@/components/common/panel-common';
import { Plus, Trash2, ZapIcon, ChevronDown, ChevronUp } from 'lucide-react';
import { Plus, Trash2, ZapIcon, ChevronDown, ChevronUp, ArrowLeftIcon } from 'lucide-react';
import { z } from 'zod';
import { ComposioTriggerDeployment } from '@/src/entities/models/composio-trigger-deployment';
import { ComposioTriggerType } from '@/src/entities/models/composio-trigger-type';
import { isToday, isThisWeek, isThisMonth } from '@/lib/utils/date';
import { listComposioTriggerDeployments, deleteComposioTriggerDeployment, createComposioTriggerDeployment, listComposioTriggerTypes } from '@/app/actions/composio.actions';
import { listComposioTriggerDeployments, deleteComposioTriggerDeployment, createComposioTriggerDeployment } from '@/app/actions/composio.actions';
import { SelectComposioToolkit } from '../../tools/components/SelectComposioToolkit';
import { ComposioTriggerTypesPanel } from '../../workflow/components/ComposioTriggerTypesPanel';
import { TriggerConfigForm } from '../../workflow/components/TriggerConfigForm';
@ -20,6 +20,8 @@ import { fetchProject } from '@/app/actions/project.actions';
type TriggerDeployment = z.infer<typeof ComposioTriggerDeployment>;
// Removed friendly name computation; backend now provides friendly trigger name
export function TriggersTab({ projectId }: { projectId: string }) {
const [triggers, setTriggers] = useState<TriggerDeployment[]>([]);
const [loading, setLoading] = useState(true);
@ -31,7 +33,6 @@ export function TriggersTab({ projectId }: { projectId: string }) {
const [isSubmittingTrigger, setIsSubmittingTrigger] = useState(false);
const [deletingTrigger, setDeletingTrigger] = useState<string | 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);
@ -46,31 +47,6 @@ export function TriggersTab({ projectId }: { projectId: string }) {
}
}, [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: [],
@ -225,10 +201,8 @@ export function TriggersTab({ projectId }: { projectId: string }) {
}, [showCreateFlow, loadTriggers]);
useEffect(() => {
if (triggers.length > 0) {
loadTriggerTypeNames();
}
}, [triggers, loadTriggerTypeNames]);
// No-op: trigger names are now derived from slug locally
}, [triggers]);
const renderTriggerList = () => {
if (loading) {
@ -261,7 +235,7 @@ export function TriggersTab({ projectId }: { projectId: string }) {
</div>
}
rightActions={
<Button variant="secondary" onClick={loadTriggers}>
<Button variant="secondary" onClick={loadTriggers} className="whitespace-nowrap">
Try Again
</Button>
}
@ -350,12 +324,25 @@ export function TriggersTab({ projectId }: { projectId: string }) {
<div className="flex items-start justify-between">
<div className="flex-1">
<a href={`/projects/${projectId}/job-rules/triggers/${trigger.id}`} className="block">
<div className="flex items-center gap-3 mb-2">
<span className="text-sm font-medium text-green-600 dark:text-green-400">
Active
</span>
<div className="flex items-center gap-3 mb-1">
{trigger.logo && (
<img
src={trigger.logo}
alt={`${trigger.toolkitSlug} logo`}
className="w-5 h-5 rounded"
/>
)}
{trigger.toolkitSlug && (
<span className="text-xs px-2 py-0.5 rounded bg-gray-100 dark:bg-gray-900 text-gray-600 dark:text-gray-400">
{trigger.toolkitSlug}
</span>
)}
</div>
<div className="h-2" />
<div className="flex items-center gap-2 mb-2">
<span className="text-sm font-medium text-green-600 dark:text-green-400">Active</span>
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
{triggerTypeNames[trigger.triggerTypeSlug] || trigger.triggerTypeSlug}
{trigger.triggerTypeName}
</span>
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
@ -422,15 +409,10 @@ export function TriggersTab({ projectId }: { projectId: string }) {
disabled={loadingMore}
variant="secondary"
size="sm"
isLoading={loadingMore}
className="whitespace-nowrap"
>
{loadingMore ? (
<>
<Spinner size="sm" />
Loading...
</>
) : (
'Load More'
)}
{loadingMore ? 'Loading...' : 'Load More'}
</Button>
</div>
)}
@ -471,8 +453,10 @@ export function TriggersTab({ projectId }: { projectId: string }) {
<Button
variant="secondary"
onClick={handleBackToList}
startContent={<ArrowLeftIcon className="w-4 h-4" />}
className="whitespace-nowrap"
>
Back to Triggers
Back to Triggers
</Button>
</div>

View file

@ -106,8 +106,7 @@ export function CreateScheduledJobRuleForm({ projectId }: { projectId: string })
title={
<div className="flex items-center gap-3">
<Link href={`/projects/${projectId}/job-rules`}>
<Button variant="secondary" size="sm">
<ArrowLeftIcon className="w-4 h-4 mr-2" />
<Button variant="secondary" size="sm" startContent={<ArrowLeftIcon className="w-4 h-4" />} className="whitespace-nowrap">
Back
</Button>
</Link>
@ -147,9 +146,9 @@ export function CreateScheduledJobRuleForm({ projectId }: { projectId: string })
onClick={addMessage}
variant="secondary"
size="sm"
className="flex items-center gap-2"
startContent={<PlusIcon className="w-4 h-4" />}
className="whitespace-nowrap"
>
<PlusIcon className="w-4 h-4" />
Add Message
</Button>
</div>
@ -197,7 +196,8 @@ export function CreateScheduledJobRuleForm({ projectId }: { projectId: string })
<Button
type="submit"
disabled={loading}
className="px-6 py-2"
isLoading={loading}
className="px-6 py-2 whitespace-nowrap"
>
{loading ? "Creating..." : "Create Rule"}
</Button>

View file

@ -81,8 +81,7 @@ export function ScheduledJobRuleView({ projectId, ruleId }: { projectId: string;
title={
<div className="flex items-center gap-3">
<Link href={`/projects/${projectId}/job-rules`}>
<Button variant="secondary" size="sm">
<ArrowLeftIcon className="w-4 h-4 mr-2" />
<Button variant="secondary" size="sm" startContent={<ArrowLeftIcon className="w-4 h-4" />} className="whitespace-nowrap">
Back
</Button>
</Link>
@ -97,9 +96,9 @@ export function ScheduledJobRuleView({ projectId, ruleId }: { projectId: string;
onClick={() => setShowDeleteConfirm(true)}
variant="secondary"
size="sm"
className="flex items-center gap-2 bg-red-50 hover:bg-red-100 text-red-700 dark:bg-red-950 dark:hover:bg-red-900 dark:text-red-400 border border-red-200 dark:border-red-800"
startContent={<Trash2Icon className="w-4 h-4" />}
className="bg-red-50 hover:bg-red-100 text-red-700 dark:bg-red-950 dark:hover:bg-red-900 dark:text-red-400 border border-red-200 dark:border-red-800 whitespace-nowrap"
>
<Trash2Icon className="w-4 h-4" />
Delete
</Button>
</div>
@ -204,6 +203,7 @@ export function ScheduledJobRuleView({ projectId, ruleId }: { projectId: string;
variant="secondary"
onClick={() => setShowDeleteConfirm(false)}
disabled={deleting}
className="whitespace-nowrap"
>
Cancel
</Button>
@ -211,19 +211,11 @@ export function ScheduledJobRuleView({ projectId, ruleId }: { projectId: string;
variant="secondary"
onClick={handleDelete}
disabled={deleting}
className="flex items-center gap-2 bg-red-50 hover:bg-red-100 text-red-700 dark:bg-red-950 dark:hover:bg-red-900 dark:text-red-400 border border-red-200 dark:border-red-800"
isLoading={deleting}
startContent={<Trash2Icon className="w-4 h-4" />}
className="bg-red-50 hover:bg-red-100 text-red-700 dark:bg-red-950 dark:hover:bg-red-900 dark:text-red-400 border border-red-200 dark:border-red-800 whitespace-nowrap"
>
{deleting ? (
<>
<Spinner size="sm" />
Deleting...
</>
) : (
<>
<Trash2Icon className="w-4 h-4" />
Delete
</>
)}
{deleting ? 'Deleting...' : 'Delete'}
</Button>
</div>
</div>

View file

@ -190,15 +190,10 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) {
disabled={loadingMore}
variant="secondary"
size="sm"
isLoading={loadingMore}
className="whitespace-nowrap"
>
{loadingMore ? (
<>
<Spinner size="sm" />
Loading...
</>
) : (
'Load More'
)}
{loadingMore ? 'Loading...' : 'Load More'}
</Button>
</div>
)}

View file

@ -1,12 +1,13 @@
'use client';
import React, { useState, useEffect, useCallback } from 'react';
import { Button, Card, CardBody, CardHeader, Spinner } from '@heroui/react';
import { Button, Card, CardBody, Spinner } from '@heroui/react';
import { ChevronLeft, ChevronRight, ZapIcon, ArrowLeft } from 'lucide-react';
import { z } from 'zod';
import { ComposioTriggerType } from '@/src/entities/models/composio-trigger-type';
import { listComposioTriggerTypes } from '@/app/actions/composio.actions';
import { ZToolkit } from "@/src/application/lib/composio/types";
import { PictureImg } from '@/components/ui/picture-img';
interface ComposioTriggerTypesPanelProps {
toolkit: z.infer<typeof ZToolkit>;
@ -151,32 +152,42 @@ export function ComposioTriggerTypesPanel({
</div>
) : (
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div className="grid gap-4 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-3">
{triggerTypes.map((triggerType) => (
<Card
key={triggerType.slug}
className="cursor-pointer hover:shadow-md transition-shadow"
<Card
key={triggerType.slug}
className="group p-6 rounded-xl transition-all duration-200 cursor-pointer bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 shadow-md dark:shadow-gray-900/20 hover:shadow-lg dark:hover:shadow-gray-900/30 hover:border-blue-300 dark:hover:border-blue-600 hover:bg-gray-50/50 hover:-translate-y-1 min-h-[200px] flex flex-col"
isPressable
onPress={() => handleTriggerTypeSelect(triggerType)}
>
<CardHeader className="flex gap-3">
<div className="flex items-center justify-center w-12 h-12 bg-blue-100 dark:bg-blue-900/30 rounded-lg">
<ZapIcon className="w-6 h-6 text-blue-600 dark:text-blue-400" />
</div>
<div className="flex flex-col">
<p className="text-base font-semibold text-gray-900 dark:text-gray-100">
<div className="flex items-start gap-3 mb-2">
{toolkit.meta?.logo ? (
<PictureImg
src={toolkit.meta.logo}
alt={`${toolkit.name} logo`}
className="w-8 h-8 rounded-md object-cover flex-shrink-0"
/>
) : (
<div className="flex items-center justify-center w-8 h-8 bg-blue-100 dark:bg-blue-900/30 rounded-md">
<ZapIcon className="w-4 h-4 text-blue-600 dark:text-blue-400" />
</div>
)}
<div className="min-w-0 flex-1">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 truncate text-left">
{triggerType.name}
</h3>
</div>
</div>
<CardBody className="pt-0 px-0 flex-1 flex flex-col">
<div className="flex-1">
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-3">
{triggerType.description}
</p>
</div>
</CardHeader>
<CardBody className="pt-0">
<p className="text-sm text-gray-600 dark:text-gray-300 line-clamp-2">
{triggerType.description}
</p>
<div className="mt-3 flex justify-end">
<Button
size="sm"
variant="flat"
<div className="mt-4 pt-4 border-t border-gray-100 dark:border-gray-700 flex justify-end">
<Button
size="sm"
variant="flat"
color="primary"
onPress={() => handleTriggerTypeSelect(triggerType)}
>

View file

@ -199,7 +199,9 @@ export function TriggerConfigForm({
onChange={(e) => handleFieldChange(fieldName, e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md
bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100
focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400"
focus:outline-none focus:ring-0 focus:ring-transparent focus:ring-offset-0
focus:border-blue-500 dark:focus:border-blue-400
transition-all duration-200"
required={isRequired}
>
<option value="">Select {property.title || fieldName}</option>
@ -234,6 +236,11 @@ export function TriggerConfigForm({
description={property.description}
isInvalid={!!fieldError}
errorMessage={fieldError}
classNames={{
base: "ring-0 !ring-0 outline-none !outline-none shadow-none !shadow-none focus:ring-0 !focus:ring-0 focus:ring-transparent !focus:ring-transparent focus-visible:ring-0 !focus-visible:ring-0 focus-visible:outline-none !focus-visible:outline-none focus-within:ring-0 !focus-within:ring-0 focus-within:shadow-none !focus-within:shadow-none",
mainWrapper: "ring-0 !ring-0 outline-none !outline-none shadow-none !shadow-none focus:ring-0 !focus:ring-0 focus:ring-transparent !focus:ring-transparent focus-visible:ring-0 !focus-visible:ring-0 focus-visible:outline-none !focus-visible:outline-none focus-within:ring-0 !focus-within:ring-0 focus-within:shadow-none !focus-within:shadow-none",
inputWrapper: "ring-0 !ring-0 outline-none !outline-none shadow-none !shadow-none focus:ring-0 !focus:ring-0 focus:ring-transparent !focus:ring-transparent focus-visible:ring-0 !focus-visible:ring-0 focus-visible:outline-none !focus-visible:outline-none focus-within:ring-0 !focus-within:ring-0 focus-within:shadow-none !focus-within:shadow-none data-[focus=true]:ring-0 group-data-[focus=true]:ring-0 data-[focus=true]:shadow-none group-data-[focus=true]:shadow-none",
}}
/>
);
})}