Add event status trigger, improve test coverage, and UI enhancements

- Add event-status-changed trigger for SmoothSchedule Activepieces piece
- Add comprehensive test coverage for payments, tickets, messaging, mobile
- Add test coverage for core services, signals, consumers, and views
- Improve Activepieces UI: templates, billing hooks, project hooks
- Update marketing automation showcase and workflow visual components
- Add public API endpoints for availability

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-20 00:19:12 -05:00
parent f3e1b8f8bf
commit 2417bb8313
51 changed files with 13823 additions and 340 deletions

View File

@@ -1,5 +1,5 @@
import { t } from 'i18next';
import { ArrowLeft, Search, SearchX } from 'lucide-react';
import { ArrowLeft, Search, SearchX, Sparkles, Building2 } from 'lucide-react';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
@@ -24,14 +24,19 @@ import {
import { LoadingSpinner } from '@/components/ui/spinner';
import { TemplateCard } from '@/features/templates/components/template-card';
import { TemplateDetailsView } from '@/features/templates/components/template-details-view';
import { useTemplates } from '@/features/templates/hooks/templates-hook';
import { useAllTemplates } from '@/features/templates/hooks/templates-hook';
import { userHooks } from '@/hooks/user-hooks';
import { PlatformRole, Template, TemplateType } from '@activepieces/shared';
import { PlatformRole, Template } from '@activepieces/shared';
export const ExplorePage = () => {
const { filteredTemplates, isLoading, search, setSearch } = useTemplates({
type: TemplateType.OFFICIAL,
});
const {
filteredCustomTemplates,
filteredOfficialTemplates,
filteredTemplates,
isLoading,
search,
setSearch,
} = useAllTemplates();
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(
null,
);
@@ -47,6 +52,20 @@ export const ExplorePage = () => {
setSelectedTemplate(null);
};
const renderTemplateGrid = (templates: Template[]) => (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{templates.map((template) => (
<TemplateCard
key={template.id}
template={template}
onSelectTemplate={(template) => {
setSelectedTemplate(template);
}}
/>
))}
</div>
);
return (
<div>
<ProjectDashboardPageHeader title={t('Explore Templates')} />
@@ -67,7 +86,7 @@ export const ExplorePage = () => {
</div>
) : (
<>
{filteredTemplates?.length === 0 && (
{filteredTemplates.length === 0 && (
<Empty className="min-h-[300px]">
<EmptyHeader className="max-w-xl">
<EmptyMedia variant="icon">
@@ -93,17 +112,38 @@ export const ExplorePage = () => {
)}
</Empty>
)}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 pb-4">
{filteredTemplates?.map((template) => (
<TemplateCard
key={template.id}
template={template}
onSelectTemplate={(template) => {
setSelectedTemplate(template);
}}
/>
))}
</div>
{/* Custom Templates Section (SmoothSchedule-specific) */}
{filteredCustomTemplates.length > 0 && (
<div className="mb-8">
<div className="flex items-center gap-2 mb-4">
<Building2 className="w-5 h-5 text-primary" />
<h2 className="text-lg font-semibold">
{t('SmoothSchedule Templates')}
</h2>
<span className="text-sm text-muted-foreground">
({filteredCustomTemplates.length})
</span>
</div>
{renderTemplateGrid(filteredCustomTemplates)}
</div>
)}
{/* Official Templates Section (from Activepieces cloud) */}
{filteredOfficialTemplates.length > 0 && (
<div className="mb-8">
<div className="flex items-center gap-2 mb-4">
<Sparkles className="w-5 h-5 text-amber-500" />
<h2 className="text-lg font-semibold">
{t('Community Templates')}
</h2>
<span className="text-sm text-muted-foreground">
({filteredOfficialTemplates.length})
</span>
</div>
{renderTemplateGrid(filteredOfficialTemplates)}
</div>
)}
</>
)}
</div>

View File

@@ -123,7 +123,15 @@ export const billingQueries = {
usePlatformSubscription: (platformId: string) => {
return useQuery({
queryKey: billingKeys.platformSubscription(platformId),
queryFn: platformBillingApi.getSubscriptionInfo,
queryFn: async () => {
try {
return await platformBillingApi.getSubscriptionInfo();
} catch {
// Return null if endpoint doesn't exist (community edition)
return null;
}
},
retry: false, // Don't retry on failure
});
},
};

View File

@@ -12,20 +12,26 @@ export const projectMembersHooks = {
const query = useQuery<ProjectMemberWithUser[]>({
queryKey: ['project-members', authenticationSession.getProjectId()],
queryFn: async () => {
const projectId = authenticationSession.getProjectId();
assertNotNullOrUndefined(projectId, 'Project ID is null');
const res = await projectMembersApi.list({
projectId: projectId,
projectRoleId: undefined,
cursor: undefined,
limit: 100,
});
return res.data;
try {
const projectId = authenticationSession.getProjectId();
assertNotNullOrUndefined(projectId, 'Project ID is null');
const res = await projectMembersApi.list({
projectId: projectId,
projectRoleId: undefined,
cursor: undefined,
limit: 100,
});
return res.data;
} catch {
// Return empty array if endpoint doesn't exist (community edition)
return [];
}
},
staleTime: Infinity,
retry: false, // Don't retry on failure
});
return {
projectMembers: query.data,
projectMembers: query.data ?? [],
isLoading: query.isLoading,
refetch: query.refetch,
};

View File

@@ -79,10 +79,14 @@ export const TemplateCard = ({
className="rounded-lg border border-solid border-dividers overflow-hidden"
>
<div className="flex items-center gap-2 p-4">
<PieceIconList
trigger={template.flows![0].trigger}
maxNumberOfIconsToShow={2}
/>
{template.flows && template.flows.length > 0 && template.flows[0].trigger ? (
<PieceIconList
trigger={template.flows[0].trigger}
maxNumberOfIconsToShow={2}
/>
) : (
<div className="h-8 w-8 rounded bg-muted" />
)}
</div>
<div className="text-sm font-medium px-4 min-h-16">{template.name}</div>
<div className="py-2 px-4 gap-1 flex items-center">

View File

@@ -13,11 +13,15 @@ export const TemplateDetailsView = ({ template }: TemplateDetailsViewProps) => {
return (
<div className="px-2">
<div className="mb-4 p-8 flex items-center justify-center gap-2 width-full bg-green-300 rounded-lg">
<PieceIconList
size="xxl"
trigger={template.flows![0].trigger}
maxNumberOfIconsToShow={3}
/>
{template.flows && template.flows.length > 0 && template.flows[0].trigger ? (
<PieceIconList
size="xxl"
trigger={template.flows[0].trigger}
maxNumberOfIconsToShow={3}
/>
) : (
<div className="h-16 w-16 rounded bg-muted" />
)}
</div>
<ScrollArea className="px-2 min-h-[156px] h-[calc(70vh-144px)] max-h-[536px]">
<div className="mb-4 text-lg font-medium font-black">

View File

@@ -1,7 +1,11 @@
import { useQuery } from '@tanstack/react-query';
import { useState } from 'react';
import { ListTemplatesRequestQuery, Template } from '@activepieces/shared';
import {
ListTemplatesRequestQuery,
Template,
TemplateType,
} from '@activepieces/shared';
import { templatesApi } from '../lib/templates-api';
@@ -9,7 +13,7 @@ export const useTemplates = (request: ListTemplatesRequestQuery) => {
const [search, setSearch] = useState<string>('');
const { data: templates, isLoading } = useQuery<Template[], Error>({
queryKey: ['templates'],
queryKey: ['templates', request.type],
queryFn: async () => {
const templates = await templatesApi.list(request);
return templates.data;
@@ -34,3 +38,86 @@ export const useTemplates = (request: ListTemplatesRequestQuery) => {
setSearch,
};
};
/**
* Hook to fetch both custom (platform) and official templates
*/
export const useAllTemplates = () => {
const [search, setSearch] = useState<string>('');
// Fetch custom templates (platform-specific)
const {
data: customTemplates,
isLoading: isLoadingCustom,
} = useQuery<Template[], Error>({
queryKey: ['templates', TemplateType.CUSTOM],
queryFn: async () => {
try {
const templates = await templatesApi.list({
type: TemplateType.CUSTOM,
});
return templates.data;
} catch {
// If custom templates fail (e.g., feature not enabled), return empty array
return [];
}
},
staleTime: 0,
});
// Fetch official templates from Activepieces cloud
const {
data: officialTemplates,
isLoading: isLoadingOfficial,
} = useQuery<Template[], Error>({
queryKey: ['templates', TemplateType.OFFICIAL],
queryFn: async () => {
try {
const templates = await templatesApi.list({
type: TemplateType.OFFICIAL,
});
return templates.data;
} catch {
return [];
}
},
staleTime: 0,
});
const isLoading = isLoadingCustom || isLoadingOfficial;
// Combine all templates
const allTemplates = [
...(customTemplates || []),
...(officialTemplates || []),
];
const filteredTemplates = allTemplates.filter((template) => {
const templateName = template.name.toLowerCase();
const templateDescription = template.description.toLowerCase();
return (
templateName.includes(search.toLowerCase()) ||
templateDescription.includes(search.toLowerCase())
);
});
// Separate filtered results by type
const filteredCustomTemplates = filteredTemplates.filter(
(t) => t.type === TemplateType.CUSTOM,
);
const filteredOfficialTemplates = filteredTemplates.filter(
(t) => t.type === TemplateType.OFFICIAL,
);
return {
customTemplates: customTemplates || [],
officialTemplates: officialTemplates || [],
allTemplates,
filteredTemplates,
filteredCustomTemplates,
filteredOfficialTemplates,
isLoading,
search,
setSearch,
};
};

View File

@@ -57,15 +57,21 @@ export const projectHooks = {
return useQuery<ProjectWithLimits[], Error>({
queryKey: ['projects', params],
queryFn: async () => {
const results = await projectApi.list({
cursor,
limit,
displayName,
...restParams,
});
return results.data;
try {
const results = await projectApi.list({
cursor,
limit,
displayName,
...restParams,
});
return results.data;
} catch {
// Return empty array if endpoint doesn't exist (embedded mode)
return [];
}
},
enabled: !displayName || displayName.length > 0,
retry: false,
});
},
useProjectsInfinite: (limit = 20) => {
@@ -77,11 +83,18 @@ export const projectHooks = {
queryKey: ['projects-infinite', limit],
getNextPageParam: (lastPage) => lastPage.next,
initialPageParam: undefined,
queryFn: ({ pageParam }) =>
projectApi.list({
cursor: pageParam as string | undefined,
limit,
}),
queryFn: async ({ pageParam }) => {
try {
return await projectApi.list({
cursor: pageParam as string | undefined,
limit,
});
} catch {
// Return empty page if endpoint doesn't exist (embedded mode)
return { data: [], next: null, previous: null };
}
},
retry: false,
});
},
useProjectsForPlatforms: () => {