diff --git a/frontend/src/connectors/ConnectWizard.test.tsx b/frontend/src/connectors/ConnectWizard.test.tsx index e6046687..8e14fbb1 100644 --- a/frontend/src/connectors/ConnectWizard.test.tsx +++ b/frontend/src/connectors/ConnectWizard.test.tsx @@ -100,6 +100,9 @@ vi.mock('./LinearPicker', async (importOriginal) => { }; }); +import actionToastReducer, { + selectActionToast, +} from '../notifications/actionToastSlice'; import notificationsReducer, { sseEventReceived, } from '../notifications/notificationsSlice'; @@ -231,6 +234,7 @@ describe('ConnectWizard', () => { reducer: { connectors: connectorsReducer, notifications: notificationsReducer, + actionToast: actionToastReducer, preference: (state = { token: null, selectedDocs: [] }) => state, conversation: (state = {}) => state, }, @@ -881,7 +885,7 @@ describe('ConnectWizard', () => { publisher: 'preset', }; - const signIn = async (props = {}) => { + const signIn = async (props = {}, onClose = vi.fn()) => { const popup = { closed: false, close: vi.fn(), location: { href: '' } }; const open = vi.spyOn(window, 'open').mockReturnValue(popup as never); mcpApi.testMCPConnection.mockResolvedValue({ @@ -905,7 +909,7 @@ describe('ConnectWizard', () => { success: true, connection: { tools: [{ ...TELEGRAM_TOOL, display_name: 'Linear' }] }, }); - const store = await render(linear, vi.fn(), props); + const store = await render(linear, onClose, props); await click('settings.connectors.wizard.signIn'); await act(async () => { store.dispatch( @@ -918,19 +922,28 @@ describe('ConnectWizard', () => { ); }); open.mockRestore(); + return store; }; - it('signs in once, then picks teams to sync with the same connection', async () => { + // The tools come first on the one screen after signing in. + const toolsFirst = () => { + const text = document.body.textContent ?? ''; + const tools = text.indexOf('settings.connectors.wizard.toolsHeading:1'); + const knowledge = text.indexOf( + 'settings.connectors.wizard.syncToKnowledge', + ); + return tools >= 0 && (knowledge < 0 || tools < knowledge); + }; + + it('signs in once, then shows its tools and picks teams to sync on one screen', async () => { service.setup.mockResolvedValue({ success: true, tools: [], sources: [{ id: 'src-1', name: 'Linear ยท Engineering' }], }); - await signIn({ purpose: 'knowledge' }); - // The tools came with the sign-in; syncing is the next question. - expect(document.body.textContent).toContain( - 'settings.connectors.wizard.chooseWhatToSync', - ); + const onClose = vi.fn(); + const store = await signIn({ purpose: 'knowledge' }, onClose); + expect(toolsFirst()).toBe(true); await click('pick-team'); await click('modals.uploadDoc.train'); const [id, body] = service.setup.mock.calls[0]; @@ -949,14 +962,18 @@ describe('ConnectWizard', () => { config: optionsToConfig(DEFAULT_RETRIEVAL_OPTIONS), }, }); - // The summary still lists the tools from the sign-in. - expect(document.body.textContent).toContain( - 'settings.connectors.wizard.toolsHeading:1', - ); + // Nothing more to show: the modal closes and says what syncs. + expect(onClose).toHaveBeenCalled(); + expect( + selectActionToast( + store.getState() as Parameters[0], + )?.variant, + ).toBe('success'); }); it('can skip syncing and keep only the tools', async () => { - await signIn({ purpose: 'knowledge' }); + const onClose = vi.fn(); + await signIn({ purpose: 'knowledge' }, onClose); const train = Array.from( document.body.querySelectorAll('button'), ).find((b) => b.textContent?.trim() === 'modals.uploadDoc.train'); @@ -964,26 +981,18 @@ describe('ConnectWizard', () => { expect(train?.disabled).toBe(true); await click('settings.connectors.wizard.skip'); expect(service.setup).not.toHaveBeenCalled(); - expect(document.body.textContent).toContain( - 'settings.connectors.wizard.doneTitle', - ); - expect(document.body.textContent).toContain( - 'settings.connectors.wizard.toolsHeading:1', - ); + expect(onClose).toHaveBeenCalled(); }); - it('asks before syncing on a plain connect, and keeps the tools', async () => { - await signIn(); + it('asks about Knowledge under its tools on a plain connect', async () => { + const onClose = vi.fn(); + await signIn({}, onClose); + expect(toolsFirst()).toBe(true); expect(knowledgeSwitch()!.getAttribute('aria-checked')).toBe('false'); expect(document.body.textContent).not.toContain('pick-team'); - await click('settings.connectors.wizard.continue'); + await click('settings.connectors.wizard.done'); expect(service.setup).not.toHaveBeenCalled(); - expect(document.body.textContent).toContain( - 'settings.connectors.wizard.toolsHeading:1', - ); - expect(document.body.textContent).toContain( - 'settings.connectors.wizard.syncLater', - ); + expect(onClose).toHaveBeenCalled(); }); it('goes straight to the summary after signing in again', async () => { diff --git a/frontend/src/connectors/ConnectWizard.tsx b/frontend/src/connectors/ConnectWizard.tsx index dffe7a02..54f6a41a 100644 --- a/frontend/src/connectors/ConnectWizard.tsx +++ b/frontend/src/connectors/ConnectWizard.tsx @@ -23,6 +23,7 @@ import { Button } from '../components/ui/button'; import { FormField } from '../components/ui/form-field'; import { Input } from '../components/ui/input'; import { Modal, ModalActions } from '../components/ui/modal'; +import { SectionHeader } from '../components/ui/section-header'; import { Select, SelectContent, @@ -49,6 +50,7 @@ import RetrievalOptions, { type RetrievalOptionsValue, } from '../settings/components/RetrievalOptions'; import useRetrievalAvailability from '../settings/components/useRetrievalAvailability'; +import { showActionToast } from '../notifications/actionToastSlice'; import type { AppDispatch } from '../store'; import { formatCount } from '../utils/dateTimeUtils'; import { ACCOUNT_NAME_MAX } from './accounts'; @@ -233,6 +235,10 @@ export default function ConnectWizard({ // done step shows what it can do. const isMcpPreset = connector.auth_kind === 'mcp_oauth' && !!connector.mcp_url; + // An MCP preset that also syncs (Linear) has its tools from the sign-in: + // one screen shows them first, then asks about Knowledge, and finishing + // it closes the wizard with no separate summary. + const toolsFirst = isMcpPreset && mode === 'connect' && tools.length > 0; const mcp = useMcpOAuth(); const mcpConfig = (): McpOAuthConfig => ({ server_url: connector.mcp_url ?? '', @@ -387,7 +393,8 @@ export default function ConnectWizard({ if (!connectionId) return; if (!wantsTools && !syncReady) { // Connected, nothing more to set up. - setStep('done'); + if (toolsFirst) finish(); + else setStep('done'); return; } setPending(true); @@ -426,6 +433,22 @@ export default function ConnectWizard({ if (wantsTools) setTools(data.tools ?? []); setSources(data.sources ?? []); refresh(); + if (toolsFirst) { + const count = (data.sources ?? []).length; + dispatch( + showActionToast({ + variant: 'success', + message: t('settings.connectors.wizard.doneSources', { + sources: t('settings.connectors.wizard.sourcesCount', { + count, + formatted: formatCount(count), + }), + }), + }), + ); + finish(); + return; + } setStep('done'); } catch { setError(t('settings.connectors.wizard.syncFailed')); @@ -615,6 +638,25 @@ export default function ConnectWizard({ {error} )} + {toolsFirst && connectionId && ( +
+ + {tools.map((tool) => ( + + ))} +
+ )} {(offerTools || askSync) && ( {offerTools && ( @@ -810,9 +852,14 @@ export default function ConnectWizard({ interpolation: { escapeValue: false }, }) : step === 'setup' - ? offerTools || !syncing - ? t('settings.connectors.wizard.chooseWhatToSetUp') - : t('settings.connectors.wizard.chooseWhatToSync') + ? toolsFirst + ? t('settings.connectors.wizard.doneTitle', { + name, + interpolation: { escapeValue: false }, + }) + : offerTools || !syncing + ? t('settings.connectors.wizard.chooseWhatToSetUp') + : t('settings.connectors.wizard.chooseWhatToSync') : t('settings.connectors.wizard.doneTitle', { name, interpolation: { escapeValue: false }, @@ -853,12 +900,16 @@ export default function ConnectWizard({ ) : step === 'setup' && !offerTools && !syncing ? ( // Only the Knowledge question, answered no: Skip would do the same. ) : step === 'setup' ? ( (mode === 'sync' ? onClose() : setStep('done'))} + onCancel={() => + mode === 'sync' ? onClose() : toolsFirst ? finish() : setStep('done') + } submitLabel={ offerTools ? t('settings.connectors.wizard.continue')