Files
windmill/frontend/src/lib/components/Password.svelte
T
Ruben Fiszel 26050f96c3 feat: support multiline secrets in resource password fields (#8637)
* feat: support multiline secrets in resource password fields

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: wire minRows through to password textarea instead of disabling it

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: auto-detect multiline in password field instead of always using textarea

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: switch to textarea on Enter keypress in password field

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: focus textarea after switching from single-line password input

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: simplify Password multiline logic and fix cursor position bug

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 09:33:26 +00:00

126 lines
3.1 KiB
Svelte

<script lang="ts">
import { createBubbler } from 'svelte/legacy'
import { tick } from 'svelte'
import Button from './common/button/Button.svelte'
import TextInput from './text_input/TextInput.svelte'
import { Eye, EyeClosed } from 'lucide-svelte'
const bubble = createBubbler()
interface Props {
password: string | undefined
placeholder?: string
disabled?: boolean
required?: boolean
small?: boolean
minRows?: number
id?: string
onKeyDown?: (event: KeyboardEvent) => void
onBlur?: (event: FocusEvent) => void
}
let {
password = $bindable(),
placeholder = '******',
disabled = false,
required = false,
small = false,
minRows,
id,
onKeyDown,
onBlur
}: Props = $props()
let red = $derived(required && (password == '' || password == undefined))
let hideValue = $state(true)
let forceMultiline = $state(false)
let isMultiline = $derived(
forceMultiline || (minRows != null && minRows > 1) || (password?.includes('\n') ?? false)
)
let textareaRef: TextInput<'textarea'> | undefined = $state()
function insertAndSwitchToMultiline(input: HTMLInputElement, text: string) {
const start = input.selectionStart
const end = input.selectionEnd
if (start != null && end != null) {
password = (password ?? '').substring(0, start) + text + (password ?? '').substring(end)
} else {
// selectionStart/End are null for type="password" inputs
password = (password ?? '') + text
}
forceMultiline = true
tick().then(() => textareaRef?.focus())
}
</script>
<div class="relative w-full {small ? 'max-w-lg' : ''}">
<div class="absolute {isMultiline ? 'top-1' : 'inset-y-0'} right-1 flex items-center z-10">
<Button
unifiedSize="sm"
onClick={() => (hideValue = !hideValue)}
iconOnly
startIcon={{ icon: hideValue ? Eye : EyeClosed }}
variant="subtle"
wrapperClasses="bg-surface-input"
/>
</div>
{#if isMultiline}
<TextInput
bind:this={textareaRef}
size="md"
error={red}
bind:value={password}
underlyingInputEl="textarea"
inputProps={{
id,
disabled,
placeholder,
rows: minRows ?? 3,
autocomplete: 'new-password',
onblur: (e) => onBlur?.(e),
onkeydown: (e) => {
onKeyDown?.(e)
bubble('keydown')(e)
},
style: hideValue ? '-webkit-text-security: disc' : ''
}}
class="pr-8"
unifiedHeight={false}
/>
{:else}
<TextInput
size="md"
error={red}
bind:value={password}
inputProps={{
id,
disabled,
placeholder,
autocomplete: 'new-password',
onblur: (e) => onBlur?.(e),
onkeydown: (e) => {
if (e.key === 'Enter') {
e.preventDefault()
insertAndSwitchToMultiline(e.currentTarget as HTMLInputElement, '\n')
return
}
onKeyDown?.(e)
bubble('keydown')(e)
},
onpaste: (e) => {
const text = e.clipboardData?.getData('text')
if (text?.includes('\n')) {
e.preventDefault()
insertAndSwitchToMultiline(e.currentTarget as HTMLInputElement, text)
}
},
type: hideValue ? 'password' : 'text'
}}
class="pr-8"
/>
{/if}
</div>
{#if red}
<div class="text-red-600 text-2xs grow">This field is required</div>
{/if}