diff --git a/apps/app/src/app/(app)/[orgId]/integrations/components/PlatformIntegrations.test.tsx b/apps/app/src/app/(app)/[orgId]/integrations/components/PlatformIntegrations.test.tsx index 31a2f3cee..b10e3ff0f 100644 --- a/apps/app/src/app/(app)/[orgId]/integrations/components/PlatformIntegrations.test.tsx +++ b/apps/app/src/app/(app)/[orgId]/integrations/components/PlatformIntegrations.test.tsx @@ -92,6 +92,7 @@ vi.mock('next/link', () => ({ // Mock next/navigation vi.mock('next/navigation', () => ({ useParams: () => ({ orgId: 'org-1' }), + useRouter: () => ({ push: vi.fn() }), useSearchParams: () => new URLSearchParams(), })); @@ -143,9 +144,10 @@ vi.mock('lucide-react', () => ({ // Mock sonner vi.mock('sonner', () => ({ - toast: { success: vi.fn(), error: vi.fn() }, + toast: { success: vi.fn(), error: vi.fn(), info: vi.fn() }, })); +import { toast } from 'sonner'; import { PlatformIntegrations } from './PlatformIntegrations'; const defaultProps = { @@ -202,4 +204,129 @@ describe('PlatformIntegrations', () => { expect(screen.getByTestId('search-input')).toBeInTheDocument(); }); }); + + describe('Employee sync import prompt', () => { + it('shows import prompt toast after Google Workspace OAuth callback', async () => { + // Override mocks for this test to simulate OAuth callback + const { useIntegrationProviders, useIntegrationConnections } = vi.mocked( + await import('@/hooks/use-integration-platform'), + ); + + vi.mocked(useIntegrationProviders).mockReturnValue({ + providers: [ + { + id: 'google-workspace', + name: 'Google Workspace', + description: 'Google Workspace admin', + category: 'Identity & Access', + logoUrl: '/google.png', + authType: 'oauth2', + oauthConfigured: true, + isActive: true, + requiredVariables: [], + mappedTasks: [], + supportsMultipleConnections: false, + }, + ] as any, + isLoading: false, + error: undefined, + refresh: vi.fn(), + }); + + vi.mocked(useIntegrationConnections).mockReturnValue({ + connections: [ + { + id: 'conn-1', + providerSlug: 'google-workspace', + status: 'active', + variables: null, + }, + ] as any, + isLoading: false, + error: undefined, + refresh: vi.fn(), + }); + + // Mock useSearchParams to simulate OAuth callback + const { useSearchParams: mockUseSearchParams } = vi.mocked( + await import('next/navigation'), + ); + vi.mocked(mockUseSearchParams).mockReturnValue( + new URLSearchParams('success=true&provider=google-workspace') as any, + ); + + setMockPermissions(ADMIN_PERMISSIONS); + + render(); + + expect(toast.success).toHaveBeenCalledWith( + 'Google Workspace connected successfully!', + ); + expect(toast.info).toHaveBeenCalledWith( + 'Import your Google Workspace users', + expect.objectContaining({ + description: 'Go to People to import and sync your team members.', + action: expect.objectContaining({ label: 'Go to People' }), + }), + ); + }); + + it('does not show import prompt for non-sync providers', async () => { + // Override mocks for this test to simulate OAuth callback for a non-sync provider + const { useIntegrationProviders, useIntegrationConnections } = vi.mocked( + await import('@/hooks/use-integration-platform'), + ); + + vi.mocked(useIntegrationProviders).mockReturnValue({ + providers: [ + { + id: 'github', + name: 'GitHub', + description: 'Code hosting', + category: 'Development', + logoUrl: '/github.png', + authType: 'oauth2', + oauthConfigured: true, + isActive: true, + requiredVariables: [], + mappedTasks: [], + supportsMultipleConnections: false, + }, + ] as any, + isLoading: false, + error: undefined, + refresh: vi.fn(), + }); + + vi.mocked(useIntegrationConnections).mockReturnValue({ + connections: [ + { + id: 'conn-2', + providerSlug: 'github', + status: 'active', + variables: null, + }, + ] as any, + isLoading: false, + error: undefined, + refresh: vi.fn(), + }); + + const { useSearchParams: mockUseSearchParams } = vi.mocked( + await import('next/navigation'), + ); + vi.mocked(mockUseSearchParams).mockReturnValue( + new URLSearchParams('success=true&provider=github') as any, + ); + + setMockPermissions(ADMIN_PERMISSIONS); + + render(); + + expect(toast.success).toHaveBeenCalledWith( + 'GitHub connected successfully!', + ); + expect(toast.info).not.toHaveBeenCalled(); + }); + }); }); diff --git a/apps/app/src/app/(app)/[orgId]/integrations/components/PlatformIntegrations.tsx b/apps/app/src/app/(app)/[orgId]/integrations/components/PlatformIntegrations.tsx index 594f8d342..700704c3e 100644 --- a/apps/app/src/app/(app)/[orgId]/integrations/components/PlatformIntegrations.tsx +++ b/apps/app/src/app/(app)/[orgId]/integrations/components/PlatformIntegrations.tsx @@ -34,7 +34,7 @@ import { } from 'lucide-react'; import Image from 'next/image'; import Link from 'next/link'; -import { useParams, useSearchParams } from 'next/navigation'; +import { useParams, useRouter, useSearchParams } from 'next/navigation'; import { useEffect, useMemo, useRef, useState } from 'react'; import { toast } from 'sonner'; import { @@ -48,6 +48,14 @@ import { TaskCard, TaskCardSkeleton } from './TaskCard'; const LOGO_TOKEN = 'pk_AZatYxV5QDSfWpRDaBxzRQ'; +// Providers that support employee sync via People > All +const EMPLOYEE_SYNC_PROVIDERS = new Set([ + 'google-workspace', + 'rippling', + 'jumpcloud', + 'ramp', +]); + // Check if a provider needs variable configuration based on manifest's required variables const providerNeedsConfiguration = ( requiredVariables: string[] | undefined, @@ -78,6 +86,7 @@ interface PlatformIntegrationsProps { export function PlatformIntegrations({ className, taskTemplates }: PlatformIntegrationsProps) { const { orgId } = useParams<{ orgId: string }>(); + const router = useRouter(); const searchParams = useSearchParams(); const { providers, isLoading: loadingProviders } = useIntegrationProviders(true); const { @@ -138,6 +147,18 @@ export function PlatformIntegrations({ className, taskTemplates }: PlatformInteg }; const handleConnectDialogSuccess = () => { + // Prompt user to import employees for providers that support sync + if (connectingProviderInfo && EMPLOYEE_SYNC_PROVIDERS.has(connectingProviderInfo.id)) { + toast.info(`Import your ${connectingProviderInfo.name} users`, { + description: + 'Go to People to import and sync your team members.', + duration: 15000, + action: { + label: 'Go to People', + onClick: () => router.push(`/${orgId}/people/all`), + }, + }); + } refreshConnections(); setConnectDialogOpen(false); setConnectingProviderInfo(null); @@ -264,6 +285,19 @@ export function PlatformIntegrations({ className, taskTemplates }: PlatformInteg if (connection && provider) { toast.success(`${provider.name} connected successfully!`); + // Prompt user to import employees for providers that support sync + if (EMPLOYEE_SYNC_PROVIDERS.has(providerSlug)) { + toast.info(`Import your ${provider.name} users`, { + description: + 'Go to People to import and sync your team members.', + duration: 15000, + action: { + label: 'Go to People', + onClick: () => router.push(`/${orgId}/people/all`), + }, + }); + } + // Set state first setSelectedConnection(connection); setSelectedProvider(provider);