From 7b380fa90341fe32ba2a595720457acd77176466 Mon Sep 17 00:00:00 2001 From: poduck Date: Sun, 21 Dec 2025 23:38:10 -0500 Subject: [PATCH] Enhance Activepieces automation flows with restore UI and publishing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add "Restore Defaults" dropdown to Automations page with confirmation - Create flows in "Defaults" folder for organization - Pre-populate trigger sample data when creating/restoring flows - Auto-publish flows (lock and enable) after creation - Fix email template context variables to match template tags - Fix dark mode logo switching in Activepieces iframe - Add iframe refresh on flow restore - Auto-populate business context (name, email, phone, address) in emails 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../community/smoothschedule/src/index.ts | 1 - .../src/lib/triggers/payment-received.ts | 93 ++++--- .../react-ui/src/components/ui/full-logo.tsx | 51 +++- .../server/api/src/app/flags/theme.ts | 4 +- activepieces-fork/publish-pieces.sh | 20 +- frontend/src/api/activepieces.ts | 36 +++ frontend/src/hooks/useActivepieces.ts | 35 +++ frontend/src/pages/Automations.tsx | 165 +++++++++++-- smoothschedule/.envs/.local/.activepieces | 3 + .../smoothschedule/identity/core/signals.py | 33 ++- .../activepieces/default_flows.py | 230 ++++++++++++++---- .../integrations/activepieces/services.py | 114 ++++++++- .../integrations/activepieces/views.py | 136 +++++++++-- .../smoothschedule/platform/api/views.py | 14 ++ 14 files changed, 786 insertions(+), 149 deletions(-) create mode 100644 frontend/src/api/activepieces.ts create mode 100644 frontend/src/hooks/useActivepieces.ts diff --git a/activepieces-fork/packages/pieces/community/smoothschedule/src/index.ts b/activepieces-fork/packages/pieces/community/smoothschedule/src/index.ts index 0b18cf30..fdb0d172 100644 --- a/activepieces-fork/packages/pieces/community/smoothschedule/src/index.ts +++ b/activepieces-fork/packages/pieces/community/smoothschedule/src/index.ts @@ -9,7 +9,6 @@ import { listCustomersAction } from './lib/actions/list-customers'; import { sendEmailAction } from './lib/actions/send-email'; import { listEmailTemplatesAction } from './lib/actions/list-email-templates'; import { eventCreatedTrigger, eventUpdatedTrigger, eventCancelledTrigger, eventStatusChangedTrigger, paymentReceivedTrigger, upcomingEventsTrigger } from './lib/triggers'; -import { API_URL } from './lib/common'; /** * SmoothSchedule Authentication diff --git a/activepieces-fork/packages/pieces/community/smoothschedule/src/lib/triggers/payment-received.ts b/activepieces-fork/packages/pieces/community/smoothschedule/src/lib/triggers/payment-received.ts index d94cce2e..cd64ce3c 100644 --- a/activepieces-fork/packages/pieces/community/smoothschedule/src/lib/triggers/payment-received.ts +++ b/activepieces-fork/packages/pieces/community/smoothschedule/src/lib/triggers/payment-received.ts @@ -38,6 +38,39 @@ interface PaymentData { } | null; } +const SAMPLE_PAYMENT_DATA: PaymentData = { + id: 12345, + payment_intent_id: 'pi_3QDEr5GvIfP3a7s90bcd1234', + amount: '50.00', + currency: 'usd', + type: 'deposit', + status: 'SUCCEEDED', + created_at: '2024-12-01T10:00:00Z', + completed_at: '2024-12-01T10:00:05Z', + event: { + id: 100, + title: 'Consultation with John Doe', + start_time: '2024-12-15T14:00:00Z', + end_time: '2024-12-15T15:00:00Z', + status: 'SCHEDULED', + deposit_amount: '50.00', + final_price: '200.00', + remaining_balance: '150.00', + }, + service: { + id: 1, + name: 'Consultation', + price: '200.00', + }, + customer: { + id: 50, + first_name: 'John', + last_name: 'Doe', + email: 'john.doe@example.com', + phone: '+1-555-0100', + }, +}; + export const paymentReceivedTrigger = createTrigger({ auth: smoothScheduleAuth, name: 'payment_received', @@ -78,15 +111,26 @@ export const paymentReceivedTrigger = createTrigger({ queryParams['type'] = paymentType; } - const payments = await makeRequest( - auth, - HttpMethod.GET, - '/payments/', - undefined, - queryParams - ); + try { + const payments = await makeRequest( + auth, + HttpMethod.GET, + '/payments/', + undefined, + queryParams + ); - return payments; + // Return real data if available, otherwise return sample data + if (payments && payments.length > 0) { + return payments; + } + } catch (error) { + // Fall through to sample data on error + console.error('Error fetching payments for sample data:', error); + } + + // Return static sample data if no real payments exist + return [SAMPLE_PAYMENT_DATA]; }, async run(context) { const auth = context.auth as SmoothScheduleAuth; @@ -121,36 +165,5 @@ export const paymentReceivedTrigger = createTrigger({ return payments; }, - sampleData: { - id: 12345, - payment_intent_id: 'pi_3QDEr5GvIfP3a7s90bcd1234', - amount: '50.00', - currency: 'usd', - type: 'deposit', - status: 'SUCCEEDED', - created_at: '2024-12-01T10:00:00Z', - completed_at: '2024-12-01T10:00:05Z', - event: { - id: 100, - title: 'Consultation with John Doe', - start_time: '2024-12-15T14:00:00Z', - end_time: '2024-12-15T15:00:00Z', - status: 'SCHEDULED', - deposit_amount: '50.00', - final_price: '200.00', - remaining_balance: '150.00', - }, - service: { - id: 1, - name: 'Consultation', - price: '200.00', - }, - customer: { - id: 50, - first_name: 'John', - last_name: 'Doe', - email: 'john.doe@example.com', - phone: '+1-555-0100', - }, - }, + sampleData: SAMPLE_PAYMENT_DATA, }); diff --git a/activepieces-fork/packages/react-ui/src/components/ui/full-logo.tsx b/activepieces-fork/packages/react-ui/src/components/ui/full-logo.tsx index 0e4a5d98..84015ef0 100644 --- a/activepieces-fork/packages/react-ui/src/components/ui/full-logo.tsx +++ b/activepieces-fork/packages/react-ui/src/components/ui/full-logo.tsx @@ -1,15 +1,64 @@ import { t } from 'i18next'; +import { useEffect, useState } from 'react'; import { flagsHooks } from '@/hooks/flags-hooks'; +import { useTheme } from '@/components/theme-provider'; const FullLogo = () => { const branding = flagsHooks.useWebsiteBranding(); + const { theme } = useTheme(); + + // Track resolved theme from DOM (handles 'system' theme correctly) + const [isDark, setIsDark] = useState(() => + document.documentElement.classList.contains('dark') + ); + + useEffect(() => { + // Update when theme changes - check the actual applied class + const checkDark = () => { + setIsDark(document.documentElement.classList.contains('dark')); + }; + checkDark(); + + // Observe class changes on documentElement + const observer = new MutationObserver((mutations) => { + for (const mutation of mutations) { + if (mutation.attributeName === 'class') { + checkDark(); + } + } + }); + observer.observe(document.documentElement, { attributes: true }); + + return () => observer.disconnect(); + }, [theme]); + + // Support dark mode by switching logo URLs + // Light logo (dark text) for light mode, dark logo (light text) for dark mode + const baseLogoUrl = branding.logos.fullLogoUrl; + + // Compute the appropriate logo URL based on theme + let logoUrl = baseLogoUrl; + if (isDark) { + // Need dark logo (light text for dark background) + if (baseLogoUrl.includes('-light.svg')) { + logoUrl = baseLogoUrl.replace('-light.svg', '-dark.svg'); + } else if (!baseLogoUrl.includes('-dark.svg')) { + logoUrl = baseLogoUrl.replace(/\.svg$/, '-dark.svg'); + } + } else { + // Need light logo (dark text for light background) + if (baseLogoUrl.includes('-dark.svg')) { + logoUrl = baseLogoUrl.replace('-dark.svg', '-light.svg'); + } + // Otherwise use base URL as-is (assumed to be light version) + } return (
{t('logo')}
diff --git a/activepieces-fork/packages/server/api/src/app/flags/theme.ts b/activepieces-fork/packages/server/api/src/app/flags/theme.ts index 771b4b76..99709d9d 100644 --- a/activepieces-fork/packages/server/api/src/app/flags/theme.ts +++ b/activepieces-fork/packages/server/api/src/app/flags/theme.ts @@ -65,8 +65,8 @@ export function generateTheme({ export const defaultTheme = generateTheme({ primaryColor: '#6e41e2', - websiteName: 'Activepieces', - fullLogoUrl: 'https://cdn.activepieces.com/brand/full-logo.png', + websiteName: 'Automation Builder', + fullLogoUrl: 'https://smoothschedule.nyc3.digitaloceanspaces.com/static/images/automation-builder-logo-light.svg', favIconUrl: 'https://cdn.activepieces.com/brand/favicon.ico', logoIconUrl: 'https://cdn.activepieces.com/brand/logo.svg', }) diff --git a/activepieces-fork/publish-pieces.sh b/activepieces-fork/publish-pieces.sh index 223e426c..4da229eb 100644 --- a/activepieces-fork/publish-pieces.sh +++ b/activepieces-fork/publish-pieces.sh @@ -141,16 +141,20 @@ main() { echo "Custom Pieces Registration" echo "============================================" - # Configure registry first (needed for both Verdaccio and fallback to npm) - if wait_for_verdaccio; then - configure_registry + # Check if Verdaccio is configured and available + if [ -n "$VERDACCIO_URL" ] && [ "$VERDACCIO_URL" != "none" ]; then + if wait_for_verdaccio; then + configure_registry - # Publish each custom piece - for piece in $CUSTOM_PIECES; do - publish_piece "$piece" || true - done + # Publish each custom piece + for piece in $CUSTOM_PIECES; do + publish_piece "$piece" || true + done + else + echo "Skipping Verdaccio publishing - pieces are pre-built in image" + fi else - echo "Skipping Verdaccio publishing - will use npm registry" + echo "Verdaccio not configured - using pre-built pieces from image" fi # Insert metadata into database diff --git a/frontend/src/api/activepieces.ts b/frontend/src/api/activepieces.ts new file mode 100644 index 00000000..4d79bf56 --- /dev/null +++ b/frontend/src/api/activepieces.ts @@ -0,0 +1,36 @@ +import api from './client'; + +export interface DefaultFlow { + flow_type: string; + display_name: string; + activepieces_flow_id: string | null; + is_modified: boolean; + is_enabled: boolean; +} + +export interface RestoreFlowResponse { + success: boolean; + flow_type: string; + message: string; +} + +export interface RestoreAllResponse { + success: boolean; + restored: string[]; + failed: string[]; +} + +export const getDefaultFlows = async (): Promise => { + const response = await api.get('/activepieces/default-flows/'); + return response.data.flows; +}; + +export const restoreFlow = async (flowType: string): Promise => { + const response = await api.post(`/activepieces/default-flows/${flowType}/restore/`); + return response.data; +}; + +export const restoreAllFlows = async (): Promise => { + const response = await api.post('/activepieces/default-flows/restore-all/'); + return response.data; +}; diff --git a/frontend/src/hooks/useActivepieces.ts b/frontend/src/hooks/useActivepieces.ts new file mode 100644 index 00000000..641d82fd --- /dev/null +++ b/frontend/src/hooks/useActivepieces.ts @@ -0,0 +1,35 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import * as activepiecesApi from '../api/activepieces'; + +export const activepiecesKeys = { + all: ['activepieces'] as const, + defaultFlows: () => [...activepiecesKeys.all, 'defaultFlows'] as const, +}; + +export const useDefaultFlows = () => { + return useQuery({ + queryKey: activepiecesKeys.defaultFlows(), + queryFn: activepiecesApi.getDefaultFlows, + staleTime: 30 * 1000, + }); +}; + +export const useRestoreFlow = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: activepiecesApi.restoreFlow, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: activepiecesKeys.defaultFlows() }); + }, + }); +}; + +export const useRestoreAllFlows = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: activepiecesApi.restoreAllFlows, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: activepiecesKeys.defaultFlows() }); + }, + }); +}; diff --git a/frontend/src/pages/Automations.tsx b/frontend/src/pages/Automations.tsx index 5b8c5679..1cae19a3 100644 --- a/frontend/src/pages/Automations.tsx +++ b/frontend/src/pages/Automations.tsx @@ -1,10 +1,13 @@ import { useState, useEffect, useRef, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useQuery } from '@tanstack/react-query'; -import { Bot, RefreshCw, AlertTriangle, Loader2, ExternalLink, Sparkles } from 'lucide-react'; +import { Bot, RefreshCw, AlertTriangle, Loader2, ExternalLink, Sparkles, RotateCcw, ChevronDown } from 'lucide-react'; import api from '../api/client'; import { usePlanFeatures } from '../hooks/usePlanFeatures'; -import { LockedSection } from '../components/UpgradePrompt'; +import { UpgradePrompt } from '../components/UpgradePrompt'; +import { useDarkMode } from '../hooks/useDarkMode'; +import { useDefaultFlows, useRestoreFlow, useRestoreAllFlows } from '../hooks/useActivepieces'; +import ConfirmationModal from '../components/ConfirmationModal'; interface ActivepiecesEmbedData { token: string; @@ -40,11 +43,22 @@ const ActivepiecesVendorEventName = { */ export default function Automations() { const { t, i18n } = useTranslation(); - const { features, loading: featuresLoading } = usePlanFeatures(); + const { permissions, isLoading: featuresLoading, canUse } = usePlanFeatures(); const iframeRef = useRef(null); const [iframeReady, setIframeReady] = useState(false); const [authenticated, setAuthenticated] = useState(false); const initSentRef = useRef(false); + const [refreshKey, setRefreshKey] = useState(0); + + // Dark mode support + const isDark = useDarkMode(); + + // Restore default flows + const { data: defaultFlows } = useDefaultFlows(); + const restoreFlow = useRestoreFlow(); + const restoreAll = useRestoreAllFlows(); + const [showFlowsMenu, setShowFlowsMenu] = useState(false); + const [confirmRestore, setConfirmRestore] = useState<{ type: 'all' | string; name: string } | null>(null); // Fetch embed token for Activepieces const { @@ -98,12 +112,13 @@ export default function Automations() { hidePageHeader: false, locale: i18n.language || 'en', initialRoute: '/flows', // Start on flows page to show sidebar + mode: isDark ? 'dark' : 'light', }, }; iframeRef.current.contentWindow.postMessage(initMessage, '*'); initSentRef.current = true; - }, [embedData?.token, i18n.language]); + }, [embedData?.token, i18n.language, isDark]); // Listen for messages from Activepieces iframe useEffect(() => { @@ -151,8 +166,27 @@ export default function Automations() { } }, [embedData?.token]); + // Reset state when theme changes to force iframe reload + useEffect(() => { + // Reset state so the iframe reinitializes with new theme + setIframeReady(false); + setAuthenticated(false); + initSentRef.current = false; + }, [isDark]); + + // Close flows menu when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (showFlowsMenu && !(event.target as Element).closest('.flows-menu-container')) { + setShowFlowsMenu(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, [showFlowsMenu]); + // Check feature access - const canAccessAutomations = features?.can_access_automations ?? true; + const canAccessAutomations = canUse('automations'); // Loading state if (isLoading || featuresLoading) { @@ -172,14 +206,7 @@ export default function Automations() { if (!canAccessAutomations) { return (
- +
); } @@ -212,8 +239,9 @@ export default function Automations() { } // Build iframe URL - use /embed route for SDK communication + // Include theme parameter for dark mode support const iframeSrc = embedData?.embedUrl - ? `${embedData.embedUrl}/embed` + ? `${embedData.embedUrl}/embed?theme=${isDark ? 'dark' : 'light'}` : ''; // Show loading until authenticated @@ -221,6 +249,47 @@ export default function Automations() { return (
+ {/* Confirmation Modal */} + setConfirmRestore(null)} + onConfirm={() => { + const refreshIframe = () => { + // Reset iframe state and increment key to force remount + initSentRef.current = false; + setAuthenticated(false); + setIframeReady(false); + setRefreshKey((k) => k + 1); + refetch(); + setConfirmRestore(null); + }; + + if (confirmRestore?.type === 'all') { + restoreAll.mutate(undefined, { + onSuccess: refreshIframe, + }); + } else if (confirmRestore) { + restoreFlow.mutate(confirmRestore.type, { + onSuccess: refreshIframe, + }); + } + }} + title={t('automations.restore.title', 'Restore Default Flow')} + message={ + confirmRestore?.type === 'all' + ? t( + 'automations.restore.allMessage', + 'This will restore all default automation flows. Any customizations will be overwritten.' + ) + : t('automations.restore.singleMessage', 'Restore "{{name}}" to its default configuration?', { + name: confirmRestore?.name, + }) + } + confirmText={t('automations.restore.confirm', 'Restore')} + variant="warning" + isLoading={restoreFlow.isPending || restoreAll.isPending} + /> + {/* Header */}
@@ -228,17 +297,9 @@ export default function Automations() {
-
-

- {t('automations.title', 'Automations')} -

-

- {t( - 'automations.subtitle', - 'Build powerful workflows to automate your business' - )} -

-
+

+ {t('automations.title', 'Automations')} +

@@ -250,12 +311,65 @@ export default function Automations() {
+ {/* Restore Default Flows dropdown */} +
+ + + {showFlowsMenu && ( +
+
+ +
+
+ {defaultFlows?.map((flow) => ( + + ))} + {(!defaultFlows || defaultFlows.length === 0) && ( +

+ {t('automations.noDefaultFlows', 'No default flows available')} +

+ )} +
+
+ )} +
+ {/* Refresh button */}