feat: Add event automation system for plugins attached to appointments
- Add GlobalEventPlugin model for auto-attaching plugins to all events - Create signals for auto-attachment on new events and rescheduling - Add API endpoints for global event plugins (CRUD, toggle, reapply) - Update CreateTaskModal with "Scheduled Task" vs "Event Automation" choice - Add option to apply to all events or future events only - Display event automations in Tasks page alongside scheduled tasks - Add EditEventAutomationModal for editing trigger and timing - Handle event reschedule - update Celery task timing on time/duration changes - Add Marketplace to Plugins menu in sidebar 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
ShoppingBag,
|
||||
@@ -19,7 +20,10 @@ import {
|
||||
Package,
|
||||
Eye,
|
||||
ChevronDown,
|
||||
AlertTriangle
|
||||
AlertTriangle,
|
||||
Clock,
|
||||
Settings,
|
||||
ArrowRight
|
||||
} from 'lucide-react';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
@@ -76,6 +80,7 @@ const detectPlatformOnlyCode = (code: string): { hasPlatformCode: boolean; warni
|
||||
|
||||
const PluginMarketplace: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedCategory, setSelectedCategory] = useState<PluginCategory | 'ALL'>('ALL');
|
||||
@@ -83,6 +88,8 @@ const PluginMarketplace: React.FC = () => {
|
||||
const [showDetailsModal, setShowDetailsModal] = useState(false);
|
||||
const [showCode, setShowCode] = useState(false);
|
||||
const [isLoadingDetails, setIsLoadingDetails] = useState(false);
|
||||
const [showWhatsNextModal, setShowWhatsNextModal] = useState(false);
|
||||
const [installedPluginId, setInstalledPluginId] = useState<string | null>(null);
|
||||
|
||||
// Fetch marketplace plugins
|
||||
const { data: plugins = [], isLoading, error } = useQuery<PluginTemplate[]>({
|
||||
@@ -109,6 +116,20 @@ const PluginMarketplace: React.FC = () => {
|
||||
},
|
||||
});
|
||||
|
||||
// Fetch installed plugins to check which are already installed
|
||||
const { data: installedPlugins = [] } = useQuery<{ template: number }[]>({
|
||||
queryKey: ['plugin-installations'],
|
||||
queryFn: async () => {
|
||||
const { data } = await api.get('/api/plugin-installations/');
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
||||
// Create a set of installed template IDs for quick lookup
|
||||
const installedTemplateIds = useMemo(() => {
|
||||
return new Set(installedPlugins.map(p => String(p.template)));
|
||||
}, [installedPlugins]);
|
||||
|
||||
// Install plugin mutation
|
||||
const installMutation = useMutation({
|
||||
mutationFn: async (templateId: string) => {
|
||||
@@ -117,10 +138,12 @@ const PluginMarketplace: React.FC = () => {
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
onSuccess: (data) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['plugin-installations'] });
|
||||
setShowDetailsModal(false);
|
||||
setSelectedPlugin(null);
|
||||
// Show What's Next modal
|
||||
setInstalledPluginId(data.id);
|
||||
setShowWhatsNextModal(true);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -322,6 +345,12 @@ const PluginMarketplace: React.FC = () => {
|
||||
Featured
|
||||
</span>
|
||||
)}
|
||||
{installedTemplateIds.has(plugin.id) && (
|
||||
<span className="inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-medium bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300">
|
||||
<CheckCircle className="h-3 w-3" />
|
||||
Installed
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{plugin.isVerified && (
|
||||
<CheckCircle className="h-5 w-5 text-blue-500 flex-shrink-0" title="Verified" />
|
||||
@@ -550,24 +579,123 @@ const PluginMarketplace: React.FC = () => {
|
||||
}}
|
||||
className="px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors font-medium"
|
||||
>
|
||||
{t('common.cancel', 'Cancel')}
|
||||
{installedTemplateIds.has(selectedPlugin.id) ? t('common.close', 'Close') : t('common.cancel', 'Cancel')}
|
||||
</button>
|
||||
{installedTemplateIds.has(selectedPlugin.id) ? (
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowDetailsModal(false);
|
||||
setSelectedPlugin(null);
|
||||
setShowCode(false);
|
||||
navigate('/plugins/my-plugins');
|
||||
}}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors font-medium"
|
||||
>
|
||||
<CheckCircle className="h-4 w-4" />
|
||||
{t('plugins.viewInstalled', 'View in My Plugins')}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={confirmInstall}
|
||||
disabled={installMutation.isPending || isLoadingDetails}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium"
|
||||
>
|
||||
{installMutation.isPending ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
{t('plugins.installing', 'Installing...')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="h-4 w-4" />
|
||||
{t('plugins.install', 'Install')}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* What's Next Modal - shown after successful install */}
|
||||
{showWhatsNextModal && selectedPlugin && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl max-w-lg w-full shadow-2xl overflow-hidden">
|
||||
{/* Success Header */}
|
||||
<div className="bg-gradient-to-r from-green-500 to-emerald-600 px-6 py-8 text-center">
|
||||
<div className="w-16 h-16 bg-white/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<CheckCircle className="w-10 h-10 text-white" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-white mb-2">
|
||||
Plugin Installed!
|
||||
</h2>
|
||||
<p className="text-green-100">
|
||||
{selectedPlugin.name} is ready to use
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* What's Next Options */}
|
||||
<div className="p-6 space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
What would you like to do?
|
||||
</h3>
|
||||
|
||||
{/* Schedule Task Option */}
|
||||
<button
|
||||
onClick={confirmInstall}
|
||||
disabled={installMutation.isPending || isLoadingDetails}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium"
|
||||
onClick={() => {
|
||||
setShowWhatsNextModal(false);
|
||||
setSelectedPlugin(null);
|
||||
navigate('/tasks');
|
||||
}}
|
||||
className="w-full flex items-center gap-4 p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:border-blue-500 dark:hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-colors text-left group"
|
||||
>
|
||||
{installMutation.isPending ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
{t('plugins.installing', 'Installing...')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="h-4 w-4" />
|
||||
{t('plugins.install', 'Install')}
|
||||
</>
|
||||
)}
|
||||
<div className="p-3 bg-blue-100 dark:bg-blue-900/30 rounded-lg">
|
||||
<Clock className="w-6 h-6 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="font-semibold text-gray-900 dark:text-white">
|
||||
Schedule it
|
||||
</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Run automatically on a schedule (hourly, daily, etc.)
|
||||
</p>
|
||||
</div>
|
||||
<ArrowRight className="w-5 h-5 text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors" />
|
||||
</button>
|
||||
|
||||
{/* Configure Option */}
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowWhatsNextModal(false);
|
||||
setSelectedPlugin(null);
|
||||
navigate('/plugins/my-plugins');
|
||||
}}
|
||||
className="w-full flex items-center gap-4 p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:border-purple-500 dark:hover:border-purple-500 hover:bg-purple-50 dark:hover:bg-purple-900/20 transition-colors text-left group"
|
||||
>
|
||||
<div className="p-3 bg-purple-100 dark:bg-purple-900/30 rounded-lg">
|
||||
<Settings className="w-6 h-6 text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="font-semibold text-gray-900 dark:text-white">
|
||||
Configure settings
|
||||
</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Set up plugin options and customize behavior
|
||||
</p>
|
||||
</div>
|
||||
<ArrowRight className="w-5 h-5 text-gray-400 group-hover:text-purple-600 dark:group-hover:text-purple-400 transition-colors" />
|
||||
</button>
|
||||
|
||||
{/* Done Option */}
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowWhatsNextModal(false);
|
||||
setSelectedPlugin(null);
|
||||
}}
|
||||
className="w-full p-3 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors text-center"
|
||||
>
|
||||
Done for now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user