Skip to content

Commit 3662650

Browse files
Add secret input emcn type
1 parent ae72147 commit 3662650

3 files changed

Lines changed: 90 additions & 29 deletions

File tree

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

Lines changed: 20 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,15 @@
33
import { type ReactNode, useCallback, useMemo, useState } from 'react'
44
import { Check, ChevronRight, Clipboard, Info } from 'lucide-react'
55
import { useShallow } from 'zustand/react/shallow'
6-
import { Checkbox, Input, Label, toast, Tooltip, Wizard } from '@/components/emcn'
6+
import {
7+
Checkbox,
8+
Input,
9+
Label,
10+
SecretInput,
11+
toast,
12+
Tooltip,
13+
Wizard,
14+
} from '@/components/emcn'
715
import { cn } from '@/lib/core/utils/cn'
816
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
917
import { useWebhookManagement } from '@/hooks/use-webhook-management'
@@ -343,7 +351,7 @@ function StepSecret({ blockId, value, onChange, disabled }: StepSecretProps) {
343351
</SubStep>
344352
<SubStep n={3}>Paste it into the field below.</SubStep>
345353
</SubStepList>
346-
<SecretInput
354+
<SecretField
347355
id={`${blockId}-wizard-signing-secret`}
348356
label='Signing Secret'
349357
value={value}
@@ -375,7 +383,7 @@ function StepToken({ blockId, value, onChange, disabled }: StepTokenProps) {
375383
</SubStep>
376384
<SubStep n={3}>Paste it into the field below.</SubStep>
377385
</SubStepList>
378-
<SecretInput
386+
<SecretField
379387
id={`${blockId}-wizard-bot-token`}
380388
label='Bot Token'
381389
value={value}
@@ -387,7 +395,7 @@ function StepToken({ blockId, value, onChange, disabled }: StepTokenProps) {
387395
)
388396
}
389397

390-
interface SecretInputProps {
398+
interface SecretFieldProps {
391399
id: string
392400
label: string
393401
value: string
@@ -397,40 +405,23 @@ interface SecretInputProps {
397405
}
398406

399407
/**
400-
* Password-style input that masks its value with bullets only while the
401-
* field is unfocused. Typing, pasting, and selection all see the real text
402-
* so users can verify what they just pasted before clicking Next.
403-
*
404-
* @remarks
405-
* Change events that fire while the field is blurred carry the bullet
406-
* string as their value (autofill, form resets, synthetic React events),
407-
* which would silently overwrite the stored secret with `•` characters.
408-
* We gate the `onChange` callback on `isFocused` so only real edits — made
409-
* while the user is interacting and the displayed value is the real text —
410-
* propagate to the store.
408+
* Label + SecretInput pair used by the signing-secret and bot-token wizard
409+
* steps. The masked-on-blur behavior lives in the emcn `SecretInput`
410+
* primitive; this wrapper just pins the label/input composition the wizard
411+
* reuses twice.
411412
*/
412-
function SecretInput({ id, label, value, onChange, disabled, placeholder }: SecretInputProps) {
413-
const [isFocused, setIsFocused] = useState<boolean>(false)
414-
const displayValue = isFocused ? value : '•'.repeat(value.length)
415-
413+
function SecretField({ id, label, value, onChange, disabled, placeholder }: SecretFieldProps) {
416414
return (
417415
<div className='space-y-1.5'>
418416
<Label htmlFor={id} className='font-medium text-[var(--text-secondary)] text-xs'>
419417
{label}
420418
</Label>
421-
<Input
419+
<SecretInput
422420
id={id}
423-
type='text'
424-
value={displayValue}
425-
onChange={(e) => {
426-
if (!isFocused) return
427-
onChange(e.target.value)
428-
}}
429-
onFocus={() => setIsFocused(true)}
430-
onBlur={() => setIsFocused(false)}
421+
value={value}
422+
onChange={onChange}
431423
disabled={disabled}
432424
placeholder={placeholder}
433-
autoComplete='off'
434425
className='h-9 text-sm'
435426
/>
436427
</div>

apps/sim/components/emcn/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ export {
124124
SModalTabsTrigger,
125125
SModalTrigger,
126126
} from './s-modal/s-modal'
127+
export { SecretInput, type SecretInputProps } from './secret-input/secret-input'
127128
export { Skeleton } from './skeleton/skeleton'
128129
export { Slider, type SliderProps } from './slider/slider'
129130
export { Switch } from './switch/switch'
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* A password-style input that masks its value with bullets only while the
3+
* field is unfocused.
4+
*
5+
* @remarks
6+
* Unlike a standard `<input type="password">`, this keeps the real text
7+
* visible while the user is actively editing — so they can verify pasted
8+
* secrets like signing tokens or API keys — and only swaps to bullets on
9+
* blur. Uses plain `type="text"` so password managers don't auto-fill.
10+
*
11+
* @example
12+
* ```tsx
13+
* import { SecretInput } from '@/components/emcn'
14+
*
15+
* <SecretInput
16+
* id='signing-secret'
17+
* value={secret}
18+
* onChange={setSecret}
19+
* placeholder='Paste your signing secret'
20+
* />
21+
* ```
22+
*/
23+
'use client'
24+
25+
import * as React from 'react'
26+
import { Input, type InputProps } from '../input/input'
27+
28+
export interface SecretInputProps
29+
extends Omit<InputProps, 'type' | 'value' | 'onChange' | 'defaultValue'> {
30+
/** Current value. Rendered as bullets when the input is not focused. */
31+
value: string
32+
/** Called with the new value on every real edit (focused-only). */
33+
onChange: (next: string) => void
34+
}
35+
36+
const SecretInput = React.forwardRef<HTMLInputElement, SecretInputProps>(
37+
({ value, onChange, onFocus, onBlur, ...props }, ref) => {
38+
const [isFocused, setIsFocused] = React.useState(false)
39+
const displayValue = isFocused ? value : '•'.repeat(value.length)
40+
41+
return (
42+
<Input
43+
ref={ref}
44+
type='text'
45+
value={displayValue}
46+
onChange={(e) => {
47+
// Guard against synthetic change events (autofill, form reset)
48+
// firing while blurred, which would otherwise overwrite the real
49+
// value with bullet characters.
50+
if (!isFocused) return
51+
onChange(e.target.value)
52+
}}
53+
onFocus={(e) => {
54+
setIsFocused(true)
55+
onFocus?.(e)
56+
}}
57+
onBlur={(e) => {
58+
setIsFocused(false)
59+
onBlur?.(e)
60+
}}
61+
autoComplete='off'
62+
{...props}
63+
/>
64+
)
65+
}
66+
)
67+
SecretInput.displayName = 'SecretInput'
68+
69+
export { SecretInput }

0 commit comments

Comments
 (0)