Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}));

Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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(<PlatformIntegrations {...defaultProps} />);

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(<PlatformIntegrations {...defaultProps} />);

expect(toast.success).toHaveBeenCalledWith(
'GitHub connected successfully!',
);
expect(toast.info).not.toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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`),
},
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicated employee sync toast logic across two paths

Low Severity

The employee sync import prompt toast.info call is identically duplicated in handleConnectDialogSuccess and the OAuth callback useEffect. Both share the same message template, description, duration, and action configuration. If the toast content, duration, or navigation target changes, both sites need independent updates, risking inconsistency. Extracting a small helper like showEmployeeSyncPrompt(providerName) would keep the logic in one place.

Additional Locations (1)
Fix in Cursor Fix in Web

refreshConnections();
setConnectDialogOpen(false);
setConnectingProviderInfo(null);
Expand Down Expand Up @@ -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);
Expand Down
Loading