Major changes: - Rename "plugins" to "automations" throughout codebase - Move automation system to dedicated app (scheduling/automations/) - Add new automation marketplace, creation, and management pages Bug fixes: - Fix payment endpoint 500 errors (use has_feature() instead of attribute) - Fix scheduler showing "0 AM" instead of "12 AM" - Fix scheduler business hours double-inversion display issue - Fix scheduler scroll-to-current-time when switching views - Fix week view centering on wrong day (use Sunday-based indexing) - Fix capacity widget overflow with many resources - Fix Recharts minWidth/minHeight console warnings 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
569 lines
24 KiB
TypeScript
569 lines
24 KiB
TypeScript
/**
|
|
* Create Automation Page
|
|
*
|
|
* Allows businesses to create custom automations with code editor,
|
|
* category selection, and visibility options.
|
|
*/
|
|
|
|
import React, { useState, useCallback } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import {
|
|
Code,
|
|
Save,
|
|
Eye,
|
|
EyeOff,
|
|
ArrowLeft,
|
|
Info,
|
|
CheckCircle,
|
|
AlertTriangle,
|
|
Mail,
|
|
BarChart3,
|
|
Users,
|
|
Calendar,
|
|
Link as LinkIcon,
|
|
Bot,
|
|
Package,
|
|
Image,
|
|
HelpCircle,
|
|
} from 'lucide-react';
|
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
|
import api from '../api/client';
|
|
import { AutomationCategory } from '../types';
|
|
import { usePlanFeatures } from '../hooks/usePlanFeatures';
|
|
|
|
// Category icon mapping
|
|
const categoryIcons: Record<AutomationCategory, React.ReactNode> = {
|
|
EMAIL: <Mail className="h-4 w-4" />,
|
|
REPORTS: <BarChart3 className="h-4 w-4" />,
|
|
CUSTOMER: <Users className="h-4 w-4" />,
|
|
BOOKING: <Calendar className="h-4 w-4" />,
|
|
INTEGRATION: <LinkIcon className="h-4 w-4" />,
|
|
AUTOMATION: <Bot className="h-4 w-4" />,
|
|
OTHER: <Package className="h-4 w-4" />,
|
|
};
|
|
|
|
// Category descriptions
|
|
const categoryDescriptions: Record<AutomationCategory, string> = {
|
|
EMAIL: 'Email notifications and automated messaging',
|
|
REPORTS: 'Analytics, reports, and data exports',
|
|
CUSTOMER: 'Customer engagement and retention',
|
|
BOOKING: 'Scheduling and booking automation',
|
|
INTEGRATION: 'Third-party service integrations',
|
|
AUTOMATION: 'General business automation',
|
|
OTHER: 'Miscellaneous automations',
|
|
};
|
|
|
|
// Default automation code template
|
|
const DEFAULT_AUTOMATION_CODE = `# My Custom Automation
|
|
#
|
|
# This automation runs on a schedule and can interact with your business data.
|
|
# Use template variables to make your automation configurable.
|
|
#
|
|
# Available template variables:
|
|
# {{PROMPT:variable_name:default_value:description}}
|
|
# {{CONTEXT:context_type}} - Access business context (CUSTOMERS, EVENTS, etc.)
|
|
# {{DATE:format}} - Current date in specified format
|
|
|
|
# Example: Get all customers who haven't booked in 30 days
|
|
inactive_days = int("{{PROMPT:inactive_days:30:Days of inactivity}}")
|
|
|
|
# Access customer data
|
|
customers = {{CONTEXT:CUSTOMERS}}
|
|
|
|
# Filter inactive customers
|
|
from datetime import datetime, timedelta
|
|
cutoff_date = datetime.now() - timedelta(days=inactive_days)
|
|
|
|
inactive_customers = [
|
|
c for c in customers
|
|
if not c.get('last_booking') or
|
|
datetime.fromisoformat(c['last_booking']) < cutoff_date
|
|
]
|
|
|
|
# Return results (will be logged)
|
|
result = {
|
|
'inactive_count': len(inactive_customers),
|
|
'customers': inactive_customers[:10], # First 10 for preview
|
|
'message': f"Found {len(inactive_customers)} inactive customers"
|
|
}
|
|
`;
|
|
|
|
interface FormData {
|
|
name: string;
|
|
shortDescription: string;
|
|
description: string;
|
|
category: AutomationCategory;
|
|
automationCode: string;
|
|
version: string;
|
|
logoUrl: string;
|
|
visibility: 'PRIVATE' | 'PUBLIC';
|
|
}
|
|
|
|
const CreateAutomation: React.FC = () => {
|
|
const { t } = useTranslation();
|
|
const navigate = useNavigate();
|
|
const queryClient = useQueryClient();
|
|
const { canUse } = usePlanFeatures();
|
|
|
|
const [formData, setFormData] = useState<FormData>({
|
|
name: '',
|
|
shortDescription: '',
|
|
description: '',
|
|
category: 'AUTOMATION',
|
|
automationCode: DEFAULT_AUTOMATION_CODE,
|
|
version: '1.0.0',
|
|
logoUrl: '',
|
|
visibility: 'PRIVATE',
|
|
});
|
|
|
|
const [showPreview, setShowPreview] = useState(false);
|
|
const [extractedVariables, setExtractedVariables] = useState<any[]>([]);
|
|
|
|
// Extract template variables from code
|
|
const extractVariables = useCallback((code: string) => {
|
|
const promptPattern = /\{\{PROMPT:([^:}]+):([^:}]*):([^}]*)\}\}/g;
|
|
const contextPattern = /\{\{CONTEXT:([^}]+)\}\}/g;
|
|
const datePattern = /\{\{DATE:([^}]+)\}\}/g;
|
|
|
|
const variables: any[] = [];
|
|
let match;
|
|
|
|
while ((match = promptPattern.exec(code)) !== null) {
|
|
variables.push({
|
|
type: 'PROMPT',
|
|
name: match[1],
|
|
default: match[2],
|
|
description: match[3],
|
|
});
|
|
}
|
|
|
|
while ((match = contextPattern.exec(code)) !== null) {
|
|
variables.push({
|
|
type: 'CONTEXT',
|
|
name: match[1],
|
|
});
|
|
}
|
|
|
|
while ((match = datePattern.exec(code)) !== null) {
|
|
variables.push({
|
|
type: 'DATE',
|
|
format: match[1],
|
|
});
|
|
}
|
|
|
|
return variables;
|
|
}, []);
|
|
|
|
// Update extracted variables when code changes
|
|
const handleCodeChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
const newCode = e.target.value;
|
|
setFormData(prev => ({ ...prev, automationCode: newCode }));
|
|
setExtractedVariables(extractVariables(newCode));
|
|
};
|
|
|
|
// Create automation mutation
|
|
const createMutation = useMutation({
|
|
mutationFn: async (data: FormData) => {
|
|
const payload = {
|
|
name: data.name,
|
|
short_description: data.shortDescription,
|
|
description: data.description,
|
|
category: data.category,
|
|
automation_code: data.automationCode,
|
|
version: data.version,
|
|
logo_url: data.logoUrl || undefined,
|
|
visibility: data.visibility,
|
|
};
|
|
const response = await api.post('/automation-templates/', payload);
|
|
return response.data;
|
|
},
|
|
onSuccess: (data) => {
|
|
queryClient.invalidateQueries({ queryKey: ['automation-templates'] });
|
|
navigate('/dashboard/automations/my-automations');
|
|
},
|
|
});
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
createMutation.mutate(formData);
|
|
};
|
|
|
|
// Check if user can create automations
|
|
const canCreateAutomations = canUse('can_create_automations');
|
|
|
|
if (!canCreateAutomations) {
|
|
return (
|
|
<div className="p-8 max-w-4xl mx-auto">
|
|
<div className="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-xl p-8 text-center">
|
|
<AlertTriangle className="h-16 w-16 mx-auto text-amber-500 mb-4" />
|
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
|
|
{t('automations.upgradeRequired', 'Upgrade Required')}
|
|
</h2>
|
|
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
|
{t('automations.upgradeToCreate', 'Automation creation is available on higher-tier plans. Upgrade your subscription to create custom automations.')}
|
|
</p>
|
|
<button
|
|
onClick={() => navigate('/dashboard/settings/billing')}
|
|
className="px-6 py-3 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors font-medium"
|
|
>
|
|
{t('automations.viewPlans', 'View Plans')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="p-8 max-w-6xl mx-auto">
|
|
{/* Header */}
|
|
<div className="mb-8">
|
|
<button
|
|
onClick={() => navigate('/dashboard/automations/my-automations')}
|
|
className="flex items-center gap-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white mb-4 transition-colors"
|
|
>
|
|
<ArrowLeft size={20} />
|
|
{t('common.back', 'Back')}
|
|
</button>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-3">
|
|
<Code className="text-brand-500" />
|
|
{t('automations.createAutomation', 'Create Custom Automation')}
|
|
</h1>
|
|
<p className="text-gray-500 dark:text-gray-400 mt-1">
|
|
{t('automations.createAutomationDescription', 'Build a custom automation for your business')}
|
|
</p>
|
|
</div>
|
|
<a
|
|
href="/help/automations"
|
|
target="_blank"
|
|
className="flex items-center gap-2 text-brand-600 dark:text-brand-400 hover:underline"
|
|
>
|
|
<HelpCircle size={18} />
|
|
{t('automations.viewDocs', 'View Documentation')}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
{/* Left Column - Basic Info */}
|
|
<div className="lg:col-span-1 space-y-6">
|
|
{/* Automation Name */}
|
|
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
|
{t('automations.basicInfo', 'Basic Information')}
|
|
</h3>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
{t('automations.automationName', 'Automation Name')} *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.name}
|
|
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500"
|
|
placeholder="e.g., Win Back Inactive Customers"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
{t('automations.shortDescription', 'Short Description')} *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.shortDescription}
|
|
onChange={(e) => setFormData(prev => ({ ...prev, shortDescription: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500"
|
|
placeholder="Brief summary for marketplace listing"
|
|
maxLength={200}
|
|
required
|
|
/>
|
|
<p className="mt-1 text-xs text-gray-500">{formData.shortDescription.length}/200</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
{t('automations.description', 'Full Description')}
|
|
</label>
|
|
<textarea
|
|
value={formData.description}
|
|
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
|
|
rows={4}
|
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 resize-none"
|
|
placeholder="Detailed description of what this automation does..."
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
{t('automations.category', 'Category')} *
|
|
</label>
|
|
<select
|
|
value={formData.category}
|
|
onChange={(e) => setFormData(prev => ({ ...prev, category: e.target.value as AutomationCategory }))}
|
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500"
|
|
>
|
|
{Object.entries(categoryDescriptions).map(([key, desc]) => (
|
|
<option key={key} value={key}>
|
|
{key} - {desc}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
{t('automations.version', 'Version')}
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.version}
|
|
onChange={(e) => setFormData(prev => ({ ...prev, version: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500"
|
|
placeholder="1.0.0"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
<Image size={16} className="inline mr-1" />
|
|
{t('automations.logoUrl', 'Logo URL')} ({t('common.optional', 'optional')})
|
|
</label>
|
|
<input
|
|
type="url"
|
|
value={formData.logoUrl}
|
|
onChange={(e) => setFormData(prev => ({ ...prev, logoUrl: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500"
|
|
placeholder="https://example.com/logo.png"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Visibility */}
|
|
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
|
{t('automations.visibility', 'Visibility')}
|
|
</h3>
|
|
|
|
<div className="space-y-3">
|
|
<label className="flex items-start gap-3 p-3 border border-gray-200 dark:border-gray-700 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
|
|
<input
|
|
type="radio"
|
|
name="visibility"
|
|
value="PRIVATE"
|
|
checked={formData.visibility === 'PRIVATE'}
|
|
onChange={(e) => setFormData(prev => ({ ...prev, visibility: 'PRIVATE' }))}
|
|
className="mt-1"
|
|
/>
|
|
<div>
|
|
<div className="flex items-center gap-2">
|
|
<EyeOff size={16} className="text-gray-500" />
|
|
<span className="font-medium text-gray-900 dark:text-white">
|
|
{t('automations.private', 'Private')}
|
|
</span>
|
|
</div>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
{t('automations.privateDescription', 'Only you can see and use this automation')}
|
|
</p>
|
|
</div>
|
|
</label>
|
|
|
|
<label className="flex items-start gap-3 p-3 border border-gray-200 dark:border-gray-700 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
|
|
<input
|
|
type="radio"
|
|
name="visibility"
|
|
value="PUBLIC"
|
|
checked={formData.visibility === 'PUBLIC'}
|
|
onChange={(e) => setFormData(prev => ({ ...prev, visibility: 'PUBLIC' }))}
|
|
className="mt-1"
|
|
/>
|
|
<div>
|
|
<div className="flex items-center gap-2">
|
|
<Eye size={16} className="text-green-500" />
|
|
<span className="font-medium text-gray-900 dark:text-white">
|
|
{t('automations.public', 'Public (Marketplace)')}
|
|
</span>
|
|
</div>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
{t('automations.publicDescription', 'Submit for review to be listed in the marketplace')}
|
|
</p>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
|
|
{formData.visibility === 'PUBLIC' && (
|
|
<div className="mt-4 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
|
<div className="flex items-start gap-2">
|
|
<Info size={16} className="text-blue-500 mt-0.5 flex-shrink-0" />
|
|
<p className="text-sm text-blue-800 dark:text-blue-200">
|
|
{t('automations.publicNote', 'Public automations require approval before appearing in the marketplace. Our team will review your code for security and quality.')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Extracted Variables */}
|
|
{extractedVariables.length > 0 && (
|
|
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
|
{t('automations.templateVariables', 'Detected Variables')}
|
|
</h3>
|
|
<div className="space-y-2">
|
|
{extractedVariables.map((v, idx) => (
|
|
<div key={idx} className="p-2 bg-gray-50 dark:bg-gray-900/50 rounded-lg text-sm">
|
|
<div className="flex items-center gap-2">
|
|
<span className={`px-1.5 py-0.5 rounded text-xs font-medium ${
|
|
v.type === 'PROMPT' ? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300' :
|
|
v.type === 'CONTEXT' ? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300' :
|
|
'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300'
|
|
}`}>
|
|
{v.type}
|
|
</span>
|
|
<span className="font-mono text-gray-900 dark:text-white">
|
|
{v.name || v.format}
|
|
</span>
|
|
</div>
|
|
{v.description && (
|
|
<p className="text-gray-500 dark:text-gray-400 mt-1 ml-4">
|
|
{v.description}
|
|
</p>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Right Column - Code Editor */}
|
|
<div className="lg:col-span-2 space-y-6">
|
|
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
|
|
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
|
<Code size={20} className="text-brand-500" />
|
|
{t('automations.automationCode', 'Automation Code')}
|
|
</h3>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPreview(!showPreview)}
|
|
className="flex items-center gap-2 text-sm text-brand-600 dark:text-brand-400 hover:underline"
|
|
>
|
|
{showPreview ? <EyeOff size={16} /> : <Eye size={16} />}
|
|
{showPreview ? t('automations.hidePreview', 'Hide Preview') : t('automations.showPreview', 'Show Preview')}
|
|
</button>
|
|
</div>
|
|
|
|
{showPreview ? (
|
|
<div className="max-h-[600px] overflow-auto">
|
|
<SyntaxHighlighter
|
|
language="python"
|
|
style={vscDarkPlus}
|
|
customStyle={{
|
|
margin: 0,
|
|
borderRadius: 0,
|
|
fontSize: '0.875rem',
|
|
}}
|
|
showLineNumbers
|
|
>
|
|
{formData.automationCode}
|
|
</SyntaxHighlighter>
|
|
</div>
|
|
) : (
|
|
<textarea
|
|
value={formData.automationCode}
|
|
onChange={handleCodeChange}
|
|
rows={25}
|
|
className="w-full px-4 py-4 bg-[#1e1e1e] text-gray-100 font-mono text-sm focus:outline-none resize-none"
|
|
placeholder="# Write your automation code here..."
|
|
spellCheck={false}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{/* Quick Reference */}
|
|
<div className="bg-gradient-to-r from-brand-50 to-indigo-50 dark:from-brand-900/20 dark:to-indigo-900/20 rounded-xl border border-brand-200 dark:border-brand-800 p-6">
|
|
<h4 className="font-semibold text-gray-900 dark:text-white mb-3">
|
|
{t('automations.quickReference', 'Quick Reference')}
|
|
</h4>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
|
|
<div>
|
|
<p className="font-medium text-gray-900 dark:text-white mb-1">Prompt Variable</p>
|
|
<code className="text-xs bg-white dark:bg-gray-800 px-2 py-1 rounded block">
|
|
{`{{PROMPT:name:default:desc}}`}
|
|
</code>
|
|
</div>
|
|
<div>
|
|
<p className="font-medium text-gray-900 dark:text-white mb-1">Context Data</p>
|
|
<code className="text-xs bg-white dark:bg-gray-800 px-2 py-1 rounded block">
|
|
{`{{CONTEXT:CUSTOMERS}}`}
|
|
</code>
|
|
</div>
|
|
<div>
|
|
<p className="font-medium text-gray-900 dark:text-white mb-1">Date Format</p>
|
|
<code className="text-xs bg-white dark:bg-gray-800 px-2 py-1 rounded block">
|
|
{`{{DATE:%Y-%m-%d}}`}
|
|
</code>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Error Message */}
|
|
{createMutation.isError && (
|
|
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
|
|
<div className="flex items-center gap-2">
|
|
<AlertTriangle className="text-red-500" size={20} />
|
|
<p className="text-red-800 dark:text-red-200">
|
|
{createMutation.error instanceof Error ? createMutation.error.message : 'Failed to create automation'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Submit Buttons */}
|
|
<div className="flex items-center justify-end gap-4 pt-4 border-t border-gray-200 dark:border-gray-700">
|
|
<button
|
|
type="button"
|
|
onClick={() => navigate('/dashboard/automations/my-automations')}
|
|
className="px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
|
>
|
|
{t('common.cancel', 'Cancel')}
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={createMutation.isPending || !formData.name || !formData.shortDescription}
|
|
className="flex items-center gap-2 px-6 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium"
|
|
>
|
|
{createMutation.isPending ? (
|
|
<>
|
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
|
{t('automations.creating', 'Creating...')}
|
|
</>
|
|
) : (
|
|
<>
|
|
<Save size={18} />
|
|
{formData.visibility === 'PUBLIC'
|
|
? t('automations.createAndSubmit', 'Create & Submit for Review')
|
|
: t('automations.createAutomation', 'Create Automation')}
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default CreateAutomation;
|