Skip to content

Commit f9c56c4

Browse files
fix(slack): preserve manual setup and accessible credential fields
1 parent bc63892 commit f9c56c4

7 files changed

Lines changed: 73 additions & 61 deletions

File tree

‎apps/sim/app/o/[organizationId]/settings/components/organization-search-slack.test.tsx‎

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ describe('Slack Search settings and shared wizard', () => {
304304
expect(document.querySelector('[role="dialog"]')).not.toHaveTextContent('Step 1')
305305
expect(document.querySelector('[role="dialog"]')).toHaveTextContent('App manifest')
306306
expect(button('Copy manifest')).toBeDefined()
307-
expect(button('Continue')).toBeDisabled()
307+
expect(button('Continue')).not.toBeDisabled()
308308
await click('Copy manifest')
309309
expect(mocks.copy).toHaveBeenCalledExactlyOnceWith('{}')
310310
expect(button('Continue')).not.toBeDisabled()
@@ -389,9 +389,7 @@ describe('Slack Search settings and shared wizard', () => {
389389
expect(container.textContent).toContain('Enabled')
390390
await action('Reconnect')
391391
expect(document.querySelector('[role="dialog"]')).toHaveTextContent('Update Slack app')
392-
expect(button('Continue')).toBeDisabled()
393-
await click('Continue')
394-
expect(document.querySelector('[role="dialog"]')).not.toHaveTextContent('Client ID')
392+
expect(button('Continue')).not.toBeDisabled()
395393
await click('Copy manifest')
396394
expect(button('Continue')).not.toBeDisabled()
397395
expect(mocks.copy).toHaveBeenCalledExactlyOnceWith('{}')
@@ -433,12 +431,31 @@ describe('Slack Search settings and shared wizard', () => {
433431
expect(document.querySelector('[role="alert"]')).toHaveTextContent('Allow clipboard access')
434432
expect(document.querySelector('a[href="https://api.slack.com/apps/A1"]')).not.toBeNull()
435433
expect(button('Copy manifest')).toBeDefined()
436-
expect(button('Continue')).toBeDisabled()
434+
expect(button('Continue')).not.toBeDisabled()
437435
await click('Copy manifest')
438436
expect(document.querySelector('[role="alert"]')).toBeNull()
439437
expect(document.querySelector('a[href="https://api.slack.com/apps/A1"]')).not.toBeNull()
440438
})
441439

440+
it.each([false, true])(
441+
'allows setup after manual manifest copying when the clipboard is unavailable (installed: %s)',
442+
async (installed) => {
443+
mocks.copy.mockRejectedValueOnce(new Error('Clipboard access denied'))
444+
await render(installed)
445+
if (installed) await action('Reconnect')
446+
else await click('Set up')
447+
await click('Copy manifest')
448+
expect(document.querySelector('[role="alert"]')).toHaveTextContent('Allow clipboard access')
449+
await act(async () => document.querySelector('summary')!.click())
450+
expect(document.querySelector('details')).toHaveAttribute('open')
451+
expect(button('Continue')).not.toBeDisabled()
452+
await click('Continue')
453+
expect(document.querySelector('[role="dialog"]')).toHaveTextContent('Install Slack app')
454+
expect(button('Continue')).toBeDisabled()
455+
expect(mocks.connect).not.toHaveBeenCalled()
456+
}
457+
)
458+
442459
it('offers the same configuration update for an app shared with Slack sources', async () => {
443460
mocks.manifest.mockReturnValue({
444461
data: {
@@ -506,14 +523,14 @@ describe('Slack Search settings and shared wizard', () => {
506523
)
507524
})
508525

509-
it('requires copying a refreshed manifest and blocks cached preparation errors', async () => {
526+
it('copies the refreshed manifest and blocks cached preparation errors', async () => {
510527
await render()
511528
await click('Set up')
512529
await click('Copy manifest')
513530
const data = { manifest: '{"display_information":{"name":"Updated"}}', existingApp: null }
514531
mocks.manifest.mockReturnValue({ data })
515532
await render()
516-
expect(button('Continue')).toBeDisabled()
533+
expect(button('Continue')).not.toBeDisabled()
517534
await click('Copy manifest')
518535
expect(mocks.copy).toHaveBeenLastCalledWith(data.manifest)
519536
expect(button('Continue')).not.toBeDisabled()

‎apps/sim/app/workspace/[workspaceId]/integrations/components/connect-slack-bot-modal/connect-slack-bot-modal.tsx‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
type ChipDropdownOption,
99
ChipInput,
1010
ChipModalField,
11-
SecretInput,
1211
Wizard,
1312
} from '@sim/emcn'
1413
import { Loader, Plus, Trash } from '@sim/emcn/icons'
@@ -616,9 +615,15 @@ interface SecretFieldProps {
616615
}
617616
function SecretField({ label, value, onChange, placeholder }: SecretFieldProps) {
618617
return (
619-
<ChipModalField type='custom' title={label}>
620-
<SecretInput value={value} onChange={onChange} placeholder={placeholder} />
621-
</ChipModalField>
618+
<ChipModalField
619+
type='input'
620+
inputType='password'
621+
title={label}
622+
value={value}
623+
onChange={onChange}
624+
placeholder={placeholder}
625+
autoComplete='off'
626+
/>
622627
)
623628
}
624629

‎apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/slack-setup-wizard.test.tsx‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,13 @@ it('collects the token before the signing secret and retains both when going bac
9191
await click('Next')
9292
await click('Next')
9393
expect(button('Next')).toBeDisabled()
94+
expect(document.querySelector('input[placeholder="xoxb-..."]')).toHaveAccessibleName('Bot Token')
9495
await fill('xoxb-...', 'xoxb-test-token')
9596
await click('Next')
9697
expect(button('Next')).toBeDisabled()
98+
expect(
99+
document.querySelector('input[placeholder="Paste your signing secret"]')
100+
).toHaveAccessibleName('Signing Secret')
97101
await fill('Paste your signing secret', 'test-secret')
98102
await click('Back')
99103
await click('Next')
@@ -103,3 +107,14 @@ it('collects the token before the signing secret and retains both when going bac
103107
expect(document.body).toHaveTextContent('verify the event Request URL')
104108
expect(document.body).not.toHaveTextContent('automatically')
105109
})
110+
111+
it('uses the existing default name when the bot name is cleared', async () => {
112+
mocks.loading = false
113+
await render()
114+
await click('Set up Slack app')
115+
await fill('Sim Workflow Bot', '')
116+
expect(button('Next')).not.toBeDisabled()
117+
await click('Next')
118+
await click('Copy manifest')
119+
expect(JSON.parse(mocks.copy.mock.calls[0][0]).display_information.name).toBe('Sim Workflow Bot')
120+
})

‎apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/slack-setup-wizard.tsx‎

Lines changed: 15 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client'
22

33
import { type ReactNode, useCallback, useMemo, useState } from 'react'
4-
import { Checkbox, Chip, ChipModalField, cn, Label, SecretInput, Tooltip, Wizard } from '@sim/emcn'
4+
import { Checkbox, Chip, ChipModalField, cn, Label, Tooltip, Wizard } from '@sim/emcn'
55
import { Check, ChevronRight, CircleInfo } from '@sim/emcn/icons'
66
import { useShallow } from 'zustand/react/shallow'
77
import { SlackAppManifest } from '@/components/integrations/slack-app-manifest'
@@ -126,7 +126,7 @@ function WizardModal({ blockId, open, onOpenChange, isPreview, disabled }: Wizar
126126
size='lg'
127127
height={MODAL_HEIGHT_CLASS}
128128
>
129-
<Wizard.Step title='Configure your bot' canAdvance={Boolean(displayAppName.trim())}>
129+
<Wizard.Step title='Configure your bot'>
130130
<StepConfigure
131131
blockId={blockId}
132132
appName={displayAppName}
@@ -142,7 +142,6 @@ function WizardModal({ blockId, open, onOpenChange, isPreview, disabled }: Wizar
142142
</Wizard.Step>
143143
<Wizard.Step title='Install and paste your Bot Token' canAdvance={Boolean(botToken?.trim())}>
144144
<StepToken
145-
blockId={blockId}
146145
value={botToken ?? ''}
147146
onChange={(v) => {
148147
if (!controlsDisabled) setBotToken(v)
@@ -152,7 +151,6 @@ function WizardModal({ blockId, open, onOpenChange, isPreview, disabled }: Wizar
152151
</Wizard.Step>
153152
<Wizard.Step title='Paste your Signing Secret' canAdvance={Boolean(signingSecret?.trim())}>
154153
<StepSecret
155-
blockId={blockId}
156154
value={signingSecret ?? ''}
157155
onChange={(v) => {
158156
if (!controlsDisabled) setSigningSecret(v)
@@ -282,13 +280,12 @@ function StepCreate({ manifestJson, canCopy, isLoading }: StepCreateProps) {
282280
}
283281

284282
interface StepSecretProps {
285-
blockId: string
286283
value: string
287284
onChange: (next: string) => void
288285
disabled: boolean
289286
}
290287

291-
function StepSecret({ blockId, value, onChange, disabled }: StepSecretProps) {
288+
function StepSecret({ value, onChange, disabled }: StepSecretProps) {
292289
return (
293290
<div className='space-y-4'>
294291
<SubStepList>
@@ -301,7 +298,6 @@ function StepSecret({ blockId, value, onChange, disabled }: StepSecretProps) {
301298
<SubStep n={3}>Paste it into the field below.</SubStep>
302299
</SubStepList>
303300
<SecretField
304-
id={`${blockId}-wizard-signing-secret`}
305301
label='Signing Secret'
306302
value={value}
307303
onChange={onChange}
@@ -313,13 +309,12 @@ function StepSecret({ blockId, value, onChange, disabled }: StepSecretProps) {
313309
}
314310

315311
interface StepTokenProps {
316-
blockId: string
317312
value: string
318313
onChange: (next: string) => void
319314
disabled: boolean
320315
}
321316

322-
function StepToken({ blockId, value, onChange, disabled }: StepTokenProps) {
317+
function StepToken({ value, onChange, disabled }: StepTokenProps) {
323318
return (
324319
<div className='space-y-4'>
325320
<SubStepList>
@@ -333,7 +328,6 @@ function StepToken({ blockId, value, onChange, disabled }: StepTokenProps) {
333328
<SubStep n={3}>Paste it into the field below.</SubStep>
334329
</SubStepList>
335330
<SecretField
336-
id={`${blockId}-wizard-bot-token`}
337331
label='Bot Token'
338332
value={value}
339333
onChange={onChange}
@@ -345,31 +339,25 @@ function StepToken({ blockId, value, onChange, disabled }: StepTokenProps) {
345339
}
346340

347341
interface SecretFieldProps {
348-
id: string
349342
label: string
350343
value: string
351344
onChange: (next: string) => void
352345
disabled: boolean
353346
placeholder?: string
354347
}
355348

356-
/**
357-
* Label + SecretInput pair used by the signing-secret and bot-token wizard
358-
* steps. The masked-on-blur behavior lives in the emcn `SecretInput`
359-
* primitive; this wrapper just pins the label/input composition the wizard
360-
* reuses twice.
361-
*/
362-
function SecretField({ id, label, value, onChange, disabled, placeholder }: SecretFieldProps) {
349+
function SecretField({ label, value, onChange, disabled, placeholder }: SecretFieldProps) {
363350
return (
364-
<ChipModalField type='custom' title={label}>
365-
<SecretInput
366-
id={id}
367-
value={value}
368-
onChange={onChange}
369-
disabled={disabled}
370-
placeholder={placeholder}
371-
/>
372-
</ChipModalField>
351+
<ChipModalField
352+
type='input'
353+
inputType='password'
354+
title={label}
355+
value={value}
356+
onChange={onChange}
357+
disabled={disabled}
358+
placeholder={placeholder}
359+
autoComplete='off'
360+
/>
373361
)
374362
}
375363

‎apps/sim/components/integrations/slack-app-manifest.test.tsx‎

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,8 @@ afterEach(async () => {
2323
vi.unstubAllGlobals()
2424
})
2525

26-
async function render(manifest: string, disabled = false, onCopy = vi.fn()) {
27-
await act(async () =>
28-
root.render(<SlackAppManifest manifest={manifest} disabled={disabled} onCopy={onCopy} />)
29-
)
26+
async function render(manifest: string, disabled = false) {
27+
await act(async () => root.render(<SlackAppManifest manifest={manifest} disabled={disabled} />))
3028
}
3129
async function copy() {
3230
await act(async () => container.querySelector('button')!.click())
@@ -53,17 +51,15 @@ describe('Slack manifest copying', () => {
5351
})
5452

5553
it('reports a failed repeat copy without stale success and allows retry', async () => {
56-
const onCopy = vi.fn()
57-
await render('{}', false, onCopy)
54+
await render('{}')
5855
await copy()
5956
writeText.mockRejectedValueOnce(new Error('Denied'))
6057
await copy()
6158
expect(container.querySelector('[role="status"]')).toBeNull()
6259
expect(container.querySelector('[role="alert"]')).toHaveTextContent('Allow clipboard access')
63-
expect(onCopy).toHaveBeenCalledTimes(1)
6460
await copy()
6561
expect(container.querySelector('[role="alert"]')).toBeNull()
66-
expect(onCopy).toHaveBeenCalledTimes(2)
62+
expect(writeText).toHaveBeenCalledTimes(3)
6763
})
6864

6965
it('does not copy an unavailable or empty manifest', async () => {

‎apps/sim/components/integrations/slack-app-manifest.tsx‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,10 @@ import { Check, Clipboard } from '@sim/emcn/icons'
77
interface SlackAppManifestProps {
88
manifest: string
99
disabled?: boolean
10-
onCopy?: (manifest: string) => void
1110
}
1211

1312
/** Shared copy action and optional preview for Slack app setup flows. */
14-
export function SlackAppManifest({ manifest, disabled, onCopy }: SlackAppManifestProps) {
13+
export function SlackAppManifest({ manifest, disabled }: SlackAppManifestProps) {
1514
const { copied, copy } = useCopyToClipboard()
1615
const [copiedManifest, setCopiedManifest] = useState<string | null>(null)
1716
const [copyFailed, setCopyFailed] = useState(false)
@@ -25,7 +24,6 @@ export function SlackAppManifest({ manifest, disabled, onCopy }: SlackAppManifes
2524
return
2625
}
2726
setCopiedManifest(manifest)
28-
onCopy?.(manifest)
2927
}
3028

3129
return (

‎apps/sim/components/integrations/slack-search-setup-wizard.tsx‎

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ export function SlackSearchSetupWizard({
5050
const [clientSecret, setClientSecret] = useState('')
5151
const [signingSecret, setSigningSecret] = useState('')
5252
const [botToken, setBotToken] = useState('')
53-
const [copiedManifest, setCopiedManifest] = useState<string | null>(null)
5453
const error = prepare.error ?? oauth.error ?? connect.error
5554
const busy = oauth.isPending || connect.isPending
5655
const configuredAppId = appId ?? prepare.data?.existingApp?.appId
@@ -207,11 +206,7 @@ export function SlackSearchSetupWizard({
207206
<ChipModalBody>
208207
{step === 'manifest' && (
209208
<ChipModalField type='custom' title='App manifest'>
210-
<SlackAppManifest
211-
manifest={prepare.data.manifest}
212-
disabled={Boolean(prepare.error)}
213-
onCopy={setCopiedManifest}
214-
/>
209+
<SlackAppManifest manifest={prepare.data.manifest} disabled={Boolean(prepare.error)} />
215210
<p className='text-[var(--text-secondary)] text-sm'>
216211
{configuredAppId
217212
? 'In your Slack app, open App Manifest, replace the JSON, and save changes.'
@@ -332,12 +327,10 @@ export function SlackSearchSetupWizard({
332327
disabled:
333328
busy ||
334329
Boolean(prepare.error) ||
335-
(step === 'manifest'
336-
? copiedManifest !== prepare.data.manifest
337-
: step === 'token'
338-
? !botToken.trim()
339-
: !installationId &&
340-
(!clientId.trim() || !clientSecret.trim() || !signingSecret.trim())),
330+
(step === 'token' && !botToken.trim()) ||
331+
(step === 'credentials' &&
332+
!installationId &&
333+
(!clientId.trim() || !clientSecret.trim() || !signingSecret.trim())),
341334
}}
342335
/>
343336
</ChipModal>

0 commit comments

Comments
 (0)