Add Activepieces integration for workflow automation

- Add Activepieces fork with SmoothSchedule custom piece
- Create integrations app with Activepieces service layer
- Add embed token endpoint for iframe integration
- Create Automations page with embedded workflow builder
- Add sidebar visibility fix for embed mode
- Add list inactive customers endpoint to Public API
- Include SmoothSchedule triggers: event created/updated/cancelled
- Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers

🤖 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-18 22:59:37 -05:00
parent 9848268d34
commit 3aa7199503
16292 changed files with 1284892 additions and 4708 deletions

View File

@@ -0,0 +1,48 @@
import { useQuery } from '@tanstack/react-query';
import { flagsHooks } from '@/hooks/flags-hooks';
import { userHooks } from '@/hooks/user-hooks';
import { authenticationApi } from '@/lib/authentication-api';
import { authenticationSession } from '@/lib/authentication-session';
import { platformApi } from '@/lib/platforms-api';
import {
ApEdition,
ApFlagId,
isNil,
Permission,
PlatformRole,
} from '@activepieces/shared';
export const useAuthorization = () => {
const { data: edition } = flagsHooks.useFlag(ApFlagId.EDITION);
const platformId = authenticationSession.getPlatformId();
const { data: projectRole, isLoading } = useQuery({
queryKey: ['project-role', authenticationSession.getProjectId()],
queryFn: async () => {
const platform = await platformApi.getCurrentPlatform();
if (platform.plan.projectRolesEnabled) {
const projectRole = await authenticationApi.getCurrentProjectRole();
return projectRole;
}
return null;
},
retry: false,
enabled:
!isNil(edition) && edition !== ApEdition.COMMUNITY && !isNil(platformId),
});
const checkAccess = (permission: Permission) => {
if (isLoading || edition === ApEdition.COMMUNITY) {
return true;
}
return projectRole?.permissions?.includes(permission) ?? true;
};
return { checkAccess };
};
export const useIsPlatformAdmin = () => {
const platformRole = userHooks.getCurrentUserPlatformRole();
return platformRole === PlatformRole.ADMIN;
};

View File

@@ -0,0 +1,46 @@
import { useSuspenseQuery } from '@tanstack/react-query';
import { ApFlagId } from '@activepieces/shared';
import { flagsApi, FlagsMap } from '../lib/flags-api';
type WebsiteBrand = {
websiteName: string;
logos: {
fullLogoUrl: string;
favIconUrl: string;
logoIconUrl: string;
};
colors: {
primary: {
default: string;
dark: string;
light: string;
};
};
};
const queryKey = ['flags'];
export const flagsHooks = {
queryKey,
useFlags: () => {
return useSuspenseQuery<FlagsMap, Error>({
queryKey,
queryFn: flagsApi.getAll,
staleTime: Infinity,
});
},
useWebsiteBranding: () => {
const { data: theme } = flagsHooks.useFlag<WebsiteBrand>(ApFlagId.THEME);
return theme!;
},
useFlag: <T>(flagId: ApFlagId) => {
const data = useSuspenseQuery<FlagsMap, Error>({
queryKey: ['flags'],
queryFn: flagsApi.getAll,
staleTime: Infinity,
}).data?.[flagId] as T | null;
return {
data,
};
},
};

View File

@@ -0,0 +1,72 @@
import {
QueryClient,
useMutation,
useSuspenseQuery,
} from '@tanstack/react-query';
import { t } from 'i18next';
import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner';
import { authenticationSession } from '@/lib/authentication-session';
import { PlatformWithoutSensitiveData } from '@activepieces/shared';
import { platformApi } from '../lib/platforms-api';
import { flagsHooks } from './flags-hooks';
export const platformHooks = {
useDeleteAccount: () => {
const navigate = useNavigate();
return useMutation({
mutationFn: async () => {
await platformApi.deleteAccount();
},
onSuccess: () => {
toast.success(t('Account deleted successfully'));
navigate('/sign-in');
},
});
},
useCurrentPlatform: () => {
const currentPlatformId = authenticationSession.getPlatformId();
const query = useSuspenseQuery({
queryKey: ['platform', currentPlatformId],
queryFn: platformApi.getCurrentPlatform,
staleTime: Infinity,
});
return {
platform: query.data,
refetch: async () => {
await query.refetch();
},
setCurrentPlatform: (
queryClient: QueryClient,
platform: PlatformWithoutSensitiveData,
) => {
queryClient.setQueryData(['platform', currentPlatformId], platform);
},
};
},
useUpdateLisenceKey: (queryClient: QueryClient) => {
const currentPlatformId = authenticationSession.getPlatformId();
return useMutation({
mutationFn: async (tempLicenseKey: string) => {
if (tempLicenseKey.trim() === '') return;
await platformApi.verifyLicenseKey(tempLicenseKey.trim());
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ['platform', currentPlatformId],
});
queryClient.invalidateQueries({
queryKey: flagsHooks.queryKey,
});
toast.success(t('License activated successfully!'));
},
onError: () => {
toast.error(t('Activation failed, invalid license key'));
},
});
},
};

View File

@@ -0,0 +1,18 @@
import { useQuery } from '@tanstack/react-query';
import { platformUserApi } from '@/lib/platform-user-api';
import { SeekPage, UserWithMetaInformation } from '@activepieces/shared';
export const platformUserHooks = {
useUsers: () => {
return useQuery<SeekPage<UserWithMetaInformation>, Error>({
queryKey: ['users'],
queryFn: async () => {
const results = await platformUserApi.list({
limit: 2000,
});
return results;
},
});
},
};

View File

@@ -0,0 +1,190 @@
import {
useQuery,
QueryClient,
useSuspenseQuery,
useInfiniteQuery,
InfiniteData,
} from '@tanstack/react-query';
import { HttpStatusCode } from 'axios';
import { t } from 'i18next';
import { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { toast } from 'sonner';
import { useEmbedding } from '@/components/embed-provider';
import { api } from '@/lib/api';
import { authenticationSession } from '@/lib/authentication-session';
import { UpdateProjectPlatformRequest } from '@activepieces/ee-shared';
import {
ApEdition,
ApFlagId,
isNil,
ProjectType,
ProjectWithLimits,
ProjectWithLimitsWithPlatform,
SeekPage,
ListProjectRequestForUserQueryParams,
} from '@activepieces/shared';
import { projectApi } from '../lib/project-api';
import { flagsHooks } from './flags-hooks';
const PERSONAL_PROJECT_NAME = 'Personal Project';
export const getProjectName = (project: ProjectWithLimits): string => {
return project.type === ProjectType.PERSONAL
? PERSONAL_PROJECT_NAME
: project.displayName;
};
export const projectHooks = {
useCurrentProject: () => {
const currentProjectId = authenticationSession.getProjectId();
const query = useSuspenseQuery<ProjectWithLimits, Error>({
queryKey: ['current-project', currentProjectId],
queryFn: projectApi.current,
});
return {
...query,
project: query.data,
updateCurrentProject,
setCurrentProject,
};
},
useProjects: (params?: ListProjectRequestForUserQueryParams) => {
const { limit = 1000, displayName, cursor, ...restParams } = params || {};
return useQuery<ProjectWithLimits[], Error>({
queryKey: ['projects', params],
queryFn: async () => {
const results = await projectApi.list({
cursor,
limit,
displayName,
...restParams,
});
return results.data;
},
enabled: !displayName || displayName.length > 0,
});
},
useProjectsInfinite: (limit = 20) => {
return useInfiniteQuery<
SeekPage<ProjectWithLimits>,
Error,
InfiniteData<SeekPage<ProjectWithLimits>>
>({
queryKey: ['projects-infinite', limit],
getNextPageParam: (lastPage) => lastPage.next,
initialPageParam: undefined,
queryFn: ({ pageParam }) =>
projectApi.list({
cursor: pageParam as string | undefined,
limit,
}),
});
},
useProjectsForPlatforms: () => {
return useQuery<ProjectWithLimitsWithPlatform[], Error>({
queryKey: ['projects-for-platforms'],
queryFn: async () => {
return projectApi.listForPlatforms();
},
});
},
useReloadPageIfProjectIdChanged: (projectId: string) => {
const { embedState } = useEmbedding();
useEffect(() => {
const handleVisibilityChange = () => {
const currentProjectId = authenticationSession.getProjectId();
if (
currentProjectId !== projectId &&
document.visibilityState === 'visible' &&
!embedState.isEmbedded
) {
window.location.reload();
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
document.removeEventListener(
'visibilitychange',
handleVisibilityChange,
);
};
}, [projectId, embedState.isEmbedded]);
},
useSwitchToProjectInParams: () => {
const { projectId: projectIdFromParams } = useParams<{
projectId: string;
}>();
const projectIdFromToken = authenticationSession.getProjectId();
const { data: edition } = flagsHooks.useFlag<ApEdition>(ApFlagId.EDITION);
const query = useSuspenseQuery<boolean, Error>({
//added currentProjectId in case user switches project and goes back to the same project
queryKey: ['switch-to-project', projectIdFromParams, projectIdFromToken],
queryFn: async () => {
if (edition === ApEdition.COMMUNITY) {
return true;
}
if (isNil(projectIdFromParams)) {
return false;
}
try {
await authenticationSession.switchToProject(projectIdFromParams);
return true;
} catch (error) {
if (
api.isError(error) &&
(error.response?.status === HttpStatusCode.BadRequest ||
error.response?.status === HttpStatusCode.Forbidden)
) {
toast.error(t('Invalid Access'), {
description: t(
'Either the project does not exist or you do not have access to it.',
),
duration: 10000,
});
}
return false;
}
},
retry: false,
staleTime: 0,
});
return {
projectIdFromParams,
projectIdFromToken,
...query,
};
},
};
const updateCurrentProject = async (
queryClient: QueryClient,
request: UpdateProjectPlatformRequest,
) => {
const currentProjectId = authenticationSession.getProjectId();
queryClient.setQueryData(['current-project', currentProjectId], {
...queryClient.getQueryData(['current-project', currentProjectId])!,
...request,
});
};
const setCurrentProject = async (
queryClient: QueryClient,
project: ProjectWithLimits,
pathName?: string,
) => {
await authenticationSession.switchToProject(project.id);
queryClient.setQueryData(['current-project'], project);
if (pathName) {
const pathNameWithNewProjectId = pathName.replace(
/\/projects\/\w+/,
`/projects/${project.id}`,
);
window.location.href = pathNameWithNewProjectId;
}
};

View File

@@ -0,0 +1,21 @@
import * as React from 'react';
const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
undefined,
);
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener('change', onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener('change', onChange);
}, []);
return !!isMobile;
}

View File

@@ -0,0 +1,34 @@
import { ApEdition, ApFlagId } from '@activepieces/shared';
import { flagsHooks } from './flags-hooks';
interface Growsumo {
data: {
email: string;
name: string;
customer_key: string;
};
createSignup: () => void;
}
declare global {
interface Window {
growsumo: Growsumo;
}
}
export const usePartnerStack = () => {
const { data: edition } = flagsHooks.useFlag<ApEdition>(ApFlagId.EDITION);
const reportSignup = (email: string, firstName: string) => {
const hasPartnerCookie = document.cookie
.split('; ')
.some((c) => c.startsWith('_ps'));
if (edition !== ApEdition.CLOUD || !hasPartnerCookie) return;
window.growsumo.data.email = email;
window.growsumo.data.name = firstName;
window.growsumo.data.customer_key = `ps_cus_key_${email}`;
window.growsumo.createSignup();
};
return { reportSignup };
};

View File

@@ -0,0 +1,41 @@
import { QueryClient, useSuspenseQuery } from '@tanstack/react-query';
import { authenticationSession } from '@/lib/authentication-session';
import { userApi } from '@/lib/user-api';
import { UserWithMetaInformationAndProject } from '@activepieces/shared';
export const userHooks = {
useCurrentUser: () => {
const userId = authenticationSession.getCurrentUserId();
const token = authenticationSession.getToken();
const expired = authenticationSession.isJwtExpired(token!);
return useSuspenseQuery<UserWithMetaInformationAndProject | null, Error>({
queryKey: ['currentUser', userId],
queryFn: async () => {
// Skip user data fetch if JWT is expired to prevent redirect to sign-in page
// This is especially important for embedding scenarios where we need to accept
// a new JWT token rather than triggering the global error handler
if (!userId || expired) {
return null;
}
try {
const result = await userApi.getCurrentUser();
return result;
} catch (error) {
console.error(error);
return null;
}
},
staleTime: Infinity,
});
},
invalidateCurrentUser: (queryClient: QueryClient) => {
const userId = authenticationSession.getCurrentUserId();
queryClient.invalidateQueries({ queryKey: ['currentUser', userId] });
},
getCurrentUserPlatformRole: () => {
const { data: user } = userHooks.useCurrentUser();
return user?.platformRole;
},
};