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:
@@ -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;
|
||||
};
|
||||
46
activepieces-fork/packages/react-ui/src/hooks/flags-hooks.ts
Normal file
46
activepieces-fork/packages/react-ui/src/hooks/flags-hooks.ts
Normal 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,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -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'));
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
190
activepieces-fork/packages/react-ui/src/hooks/project-hooks.ts
Normal file
190
activepieces-fork/packages/react-ui/src/hooks/project-hooks.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
21
activepieces-fork/packages/react-ui/src/hooks/use-mobile.tsx
Normal file
21
activepieces-fork/packages/react-ui/src/hooks/use-mobile.tsx
Normal 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;
|
||||
}
|
||||
@@ -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 };
|
||||
};
|
||||
41
activepieces-fork/packages/react-ui/src/hooks/user-hooks.ts
Normal file
41
activepieces-fork/packages/react-ui/src/hooks/user-hooks.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user