From 4aa1f012107469fe2da7e17ef72801aebd4ac0d0 Mon Sep 17 00:00:00 2001 From: Guilhem Date: Mon, 16 Feb 2026 14:51:06 +0000 Subject: [PATCH] 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 --- frontend/src/lib/components/AddUser.svelte | 22 +++++--- frontend/src/lib/components/DBManager.svelte | 1 - .../src/lib/components/select/Select.svelte | 50 +++++++++++-------- .../CustomInstanceDbSelect.svelte | 1 - 4 files changed, 43 insertions(+), 31 deletions(-) diff --git a/frontend/src/lib/components/AddUser.svelte b/frontend/src/lib/components/AddUser.svelte index 75c69a440b..4bebf7794c 100644 --- a/frontend/src/lib/components/AddUser.svelte +++ b/frontend/src/lib/components/AddUser.svelte @@ -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} - + {/if} {#if emailError} - {emailError} + {/if} {#if !automateUsernameCreation} Username - + {/if} Role @@ -172,7 +176,9 @@ username = undefined }) }} - disabled={email === undefined || !!emailError || (!automateUsernameCreation && username === undefined)} + disabled={email === undefined || + !!emailError || + (!automateUsernameCreation && username === undefined)} > Add diff --git a/frontend/src/lib/components/DBManager.svelte b/frontend/src/lib/components/DBManager.svelte index 3000eb2d5f..b57edefc51 100644 --- a/frontend/src/lib/components/DBManager.svelte +++ b/frontend/src/lib/components/DBManager.svelte @@ -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() diff --git a/frontend/src/lib/components/select/Select.svelte b/frontend/src/lib/components/select/Select.svelte index f57f21c300..3ba4b37b06 100644 --- a/frontend/src/lib/components/select/Select.svelte +++ b/frontend/src/lib/components/select/Select.svelte @@ -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) { 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 - })
(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 @@ (value = i)} placeholder="Search or create..." - showPlaceholderOnOpen items={onlySelectedTags} id="custom-instance-db-select" disabled={!$isCustomInstanceDbEnabled}