mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-22 08:02:19 +00:00
refactor: replace Select placeholder hack with autocomplete model
Replace the bind:value getter/setter and placeholder-as-display hack with
a standard controlled input pattern (value={inputValue} + oninput handler).
- Remove showPlaceholderOnOpen prop (replaced by default behavior)
- Add allowUserInput prop: typed text syncs to value in real-time
- Separate rawLabel (for filtering) from displayText (for transformed display)
- Hide dropdown when no items match in allowUserInput mode
- Simplify AddUser to use allowUserInput instead of createText/onCreateItem
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -119,23 +119,27 @@
|
||||
items={instanceEmails}
|
||||
bind:value={email}
|
||||
placeholder="Select or type an email"
|
||||
createText="Use custom email:"
|
||||
onCreateItem={(v) => {
|
||||
email = v
|
||||
}}
|
||||
allowUserInput
|
||||
disablePortal={true}
|
||||
error={!!emailError}
|
||||
/>
|
||||
{:else}
|
||||
<TextInput inputProps={{ type: 'email', onkeyup: handleKeyUp, placeholder: 'email' }} bind:value={email} error={!!emailError} />
|
||||
<TextInput
|
||||
inputProps={{ type: 'email', onkeyup: handleKeyUp, placeholder: 'email' }}
|
||||
bind:value={email}
|
||||
error={!!emailError}
|
||||
/>
|
||||
{/if}
|
||||
{#if emailError}
|
||||
<span class="text-2xs text-red-500 mt-0.5">{emailError}</span>
|
||||
<InputError error={emailError} />
|
||||
{/if}
|
||||
|
||||
{#if !automateUsernameCreation}
|
||||
<span class="text-xs mb-1 pt-2 leading-6">Username</span>
|
||||
<TextInput inputProps={{ type: 'text', onkeyup: handleKeyUp, placeholder: 'username' }} bind:value={username} />
|
||||
<TextInput
|
||||
inputProps={{ type: 'text', onkeyup: handleKeyUp, placeholder: 'username' }}
|
||||
bind:value={username}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<span class="text-xs mb-1 pt-6 leading-6">Role</span>
|
||||
@@ -172,7 +176,9 @@
|
||||
username = undefined
|
||||
})
|
||||
}}
|
||||
disabled={email === undefined || !!emailError || (!automateUsernameCreation && username === undefined)}
|
||||
disabled={email === undefined ||
|
||||
!!emailError ||
|
||||
(!automateUsernameCreation && username === undefined)}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
|
||||
@@ -252,7 +252,6 @@
|
||||
transformInputSelectedText={(s) => `Schema: ${s}`}
|
||||
RightIcon={ChevronDownIcon}
|
||||
placeholder="Search or create schema..."
|
||||
showPlaceholderOnOpen
|
||||
onCreateItem={(schema) => {
|
||||
schema = schema.trim().replace(/[^a-zA-Z0-9_]/g, '')
|
||||
if (dbType === 'snowflake') schema = schema.toUpperCase()
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
itemLabelWrapperClasses,
|
||||
itemButtonWrapperClasses,
|
||||
size = 'md',
|
||||
showPlaceholderOnOpen = false,
|
||||
allowUserInput = false,
|
||||
transformInputSelectedText,
|
||||
groupBy,
|
||||
sortBy,
|
||||
@@ -76,7 +76,8 @@
|
||||
itemLabelWrapperClasses?: string
|
||||
itemButtonWrapperClasses?: string
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
showPlaceholderOnOpen?: boolean
|
||||
/** When true, any text typed by the user becomes the value when the dropdown closes */
|
||||
allowUserInput?: boolean
|
||||
transformInputSelectedText?: (text: string) => string
|
||||
groupBy?: (item: Item) => string
|
||||
sortBy?: (a: Item, b: Item) => number
|
||||
@@ -99,6 +100,23 @@
|
||||
return untrack(() => processItems(args))
|
||||
})
|
||||
|
||||
let valueEntry = $derived(value && processedItems?.find((item) => deepEqual(item.value, value)))
|
||||
|
||||
let rawLabel = $derived(valueEntry?.label ?? getLabel({ value }) ?? '')
|
||||
|
||||
let displayText = $derived(transformInputSelectedText?.(rawLabel) ?? rawLabel)
|
||||
|
||||
let inputValue = $derived(open ? filterText : displayText)
|
||||
|
||||
let hasFilteredItems = $derived(
|
||||
!filterText ||
|
||||
processedItems?.some(
|
||||
(item) => !item.__is_create && item.label?.toLowerCase().includes(filterText.toLowerCase())
|
||||
)
|
||||
)
|
||||
|
||||
let dropdownVisible = $derived(open && (!allowUserInput || hasFilteredItems))
|
||||
|
||||
$effect(() => {
|
||||
if (filterText) open = true
|
||||
})
|
||||
@@ -107,15 +125,13 @@
|
||||
filterText = ''
|
||||
} else {
|
||||
untrack(() => {
|
||||
if (value && !valueEntry) {
|
||||
filterText = inputText
|
||||
if (rawLabel) {
|
||||
filterText = rawLabel
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
let valueEntry = $derived(value && processedItems?.find((item) => deepEqual(item.value, value)))
|
||||
|
||||
function setValue(item: ProcessedItem<Value>) {
|
||||
if (item.__is_create && onCreateItem) {
|
||||
onCreateItem(item.value)
|
||||
@@ -131,11 +147,6 @@
|
||||
if (onClear) onClear()
|
||||
else value = undefined
|
||||
}
|
||||
|
||||
let inputText = $derived.by(() => {
|
||||
let text = valueEntry?.label ?? getLabel({ value }) ?? ''
|
||||
return transformInputSelectedText?.(text) ?? text
|
||||
})
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -168,12 +179,8 @@
|
||||
{autofocus}
|
||||
{disabled}
|
||||
type="text"
|
||||
bind:value={() => (open ? filterText : inputText), (v) => open && (filterText = v)}
|
||||
placeholder={loading && !value
|
||||
? 'Loading...'
|
||||
: value && !showPlaceholderOnOpen && !(open && !valueEntry)
|
||||
? inputText
|
||||
: placeholder}
|
||||
value={inputValue}
|
||||
placeholder={loading && !value ? 'Loading...' : placeholder}
|
||||
style={containerStyle}
|
||||
class={twMerge(
|
||||
inputBaseClass,
|
||||
@@ -182,16 +189,17 @@
|
||||
inputBorderClass({ error, forceFocus: open }),
|
||||
'w-full',
|
||||
open ? '' : 'cursor-pointer',
|
||||
// Show value as placeholder when opening the dropdown and the search is empty
|
||||
!value ? 'placeholder-hint' : '!placeholder-primary',
|
||||
'placeholder-hint',
|
||||
(clearable || RightIcon) && !disabled && value ? 'pr-8' : '',
|
||||
inputClass ?? ''
|
||||
)}
|
||||
autocomplete="off"
|
||||
oninput={(e) => {
|
||||
// Explicitly open dropdown if closed and update filterText
|
||||
if (!open) open = true
|
||||
filterText = e.currentTarget.value
|
||||
if (allowUserInput) {
|
||||
value = filterText || undefined
|
||||
}
|
||||
}}
|
||||
onpointerdown={() => (open = true)}
|
||||
bind:this={inputEl}
|
||||
@@ -200,7 +208,7 @@
|
||||
<SelectDropdown
|
||||
{disablePortal}
|
||||
onSelectValue={setValue}
|
||||
{open}
|
||||
open={dropdownVisible}
|
||||
{processedItems}
|
||||
{value}
|
||||
{disabled}
|
||||
|
||||
@@ -50,7 +50,6 @@
|
||||
bind:value
|
||||
onCreateItem={(i) => (value = i)}
|
||||
placeholder="Search or create..."
|
||||
showPlaceholderOnOpen
|
||||
items={onlySelectedTags}
|
||||
id="custom-instance-db-select"
|
||||
disabled={!$isCustomInstanceDbEnabled}
|
||||
|
||||
Reference in New Issue
Block a user