diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e4b6658b41..ce57c0026d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -132,7 +132,6 @@ "svelte-check": "^4.0.0", "svelte-floating-ui": "^1.5.8", "svelte-highlight": "^7.6.0", - "svelte-multiselect": "^11.0.0-rc.1", "svelte-popperjs": "^1.3.2", "svelte-preprocess": "^6.0.0", "svelte-range-slider-pips": "^2.3.1", @@ -11862,15 +11861,6 @@ "resolved": "https://registry.npmjs.org/svelte-infinite-loading/-/svelte-infinite-loading-1.4.0.tgz", "integrity": "sha512-Jo+f/yr/HmZQuIiiKKzAHVFXdAUWHW2RBbrcQTil8JVk1sCm/riy7KTJVzjBgQvHasrFQYKF84zvtc9/Y4lFYg==" }, - "node_modules/svelte-multiselect": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/svelte-multiselect/-/svelte-multiselect-11.1.0.tgz", - "integrity": "sha512-D93t6GlOV//gU7upR59uCd755Hq6OWhqv9ddhOXBDN/mEZj5XOAYHwDjUjePUKlE+bI+jry1BlZef8Mof2Y+YA==", - "dev": true, - "peerDependencies": { - "svelte": "^5.8.0" - } - }, "node_modules/svelte-popperjs": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/svelte-popperjs/-/svelte-popperjs-1.3.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 9cf40edea3..cab3c1d9a6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -57,7 +57,6 @@ "svelte-check": "^4.0.0", "svelte-floating-ui": "^1.5.8", "svelte-highlight": "^7.6.0", - "svelte-multiselect": "^11.0.0-rc.1", "svelte-popperjs": "^1.3.2", "svelte-preprocess": "^6.0.0", "svelte-range-slider-pips": "^2.3.1", diff --git a/frontend/src/lib/components/ArgEnum.svelte b/frontend/src/lib/components/ArgEnum.svelte index b1f94d39f0..d87d34ea33 100644 --- a/frontend/src/lib/components/ArgEnum.svelte +++ b/frontend/src/lib/components/ArgEnum.svelte @@ -1,7 +1,7 @@ diff --git a/frontend/src/lib/components/DefaultTagsInner.svelte b/frontend/src/lib/components/DefaultTagsInner.svelte index 5e4e824787..ab715770de 100644 --- a/frontend/src/lib/components/DefaultTagsInner.svelte +++ b/frontend/src/lib/components/DefaultTagsInner.svelte @@ -7,11 +7,12 @@ import { enterpriseLicense, superadmin } from '$lib/stores' import { DEFAULT_TAGS_PER_WORKSPACE_SETTING, DEFAULT_TAGS_WORKSPACES_SETTING } from '$lib/consts' import Toggle from './Toggle.svelte' - import MultiSelectWrapper from './multiselect/MultiSelectWrapper.svelte' + import MultiSelect from './select/MultiSelect.svelte' + import { safeSelectItems } from './select/utils.svelte' let defaultTags: string[] | undefined = undefined export let defaultTagPerWorkspace: boolean | undefined = undefined - export let defaultTagWorkspaces: string[] | undefined = undefined + export let defaultTagWorkspaces: string[] = [] let limitToWorkspaces = false let workspaces: string[] = [] @@ -62,7 +63,7 @@ {/each} -
+
{#if limitToWorkspaces} - {/if} diff --git a/frontend/src/lib/components/FolderEditor.svelte b/frontend/src/lib/components/FolderEditor.svelte index 5ae36a7e24..38f980f042 100644 --- a/frontend/src/lib/components/FolderEditor.svelte +++ b/frontend/src/lib/components/FolderEditor.svelte @@ -18,7 +18,8 @@ import Label from './Label.svelte' import { sendUserToast } from '$lib/toast' import { createEventDispatcher } from 'svelte' - import Select from './Select.svelte' + import Select from './select/Select.svelte' + import { safeSelectItems } from './select/utils.svelte' export let name: string let can_write = false @@ -197,7 +198,7 @@ ownerKind === 'user' ? usernames.filter((x) => !perms?.map((y) => y.owner_name).includes('u/' + x)) : groups.filter((x) => !perms?.map((y) => y.owner_name).includes('g/' + x))} - {#if ownerKind == 'group'} diff --git a/frontend/src/lib/components/LightweightResourcePicker.svelte b/frontend/src/lib/components/LightweightResourcePicker.svelte index 50da38607b..87570466b4 100644 --- a/frontend/src/lib/components/LightweightResourcePicker.svelte +++ b/frontend/src/lib/components/LightweightResourcePicker.svelte @@ -8,7 +8,7 @@ import type { AppViewerContext } from './apps/types' import { sendUserToast } from '$lib/toast' import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted' - import Select from './Select.svelte' + import Select from './select/Select.svelte' const dispatch = createEventDispatcher() const dispatchIfMounted = createDispatcherIfMounted(dispatch) diff --git a/frontend/src/lib/components/ResourcePicker.svelte b/frontend/src/lib/components/ResourcePicker.svelte index a07596fc34..f136fd32a6 100644 --- a/frontend/src/lib/components/ResourcePicker.svelte +++ b/frontend/src/lib/components/ResourcePicker.svelte @@ -10,7 +10,7 @@ import { Pen, Plus, RotateCw } from 'lucide-svelte' import { sendUserToast } from '$lib/toast' import { isDbType } from './apps/components/display/dbtable/utils' - import Select from './Select.svelte' + import Select from './select/Select.svelte' import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted' const dispatch = createEventDispatcher() diff --git a/frontend/src/lib/components/SavedInputsV2.svelte b/frontend/src/lib/components/SavedInputsV2.svelte index 538f3ce05f..0fb4f89e58 100644 --- a/frontend/src/lib/components/SavedInputsV2.svelte +++ b/frontend/src/lib/components/SavedInputsV2.svelte @@ -10,7 +10,8 @@ import { ExternalLink, Search } from 'lucide-svelte' import { Popover } from './meltComponents' import SchemaForm from './SchemaForm.svelte' - import MultiSelect from './multiselect/MultiSelectWrapper.svelte' + import MultiSelect from './select/MultiSelect.svelte' + import { safeSelectItems } from './select/utils.svelte' const dispatch = createEventDispatcher() interface Props { @@ -142,7 +143,6 @@ {/snippet} {#snippet content()} -
Search by args
@@ -175,11 +175,10 @@
{#key filteredSchema} diff --git a/frontend/src/lib/components/ScriptPicker.svelte b/frontend/src/lib/components/ScriptPicker.svelte index 0bb0a9caca..c4fe27167a 100644 --- a/frontend/src/lib/components/ScriptPicker.svelte +++ b/frontend/src/lib/components/ScriptPicker.svelte @@ -5,7 +5,7 @@ import { base } from '$lib/base' import { createEventDispatcher, untrack } from 'svelte' - import Select from './Select.svelte' + import Select from './select/Select.svelte' import { getScriptByPath } from '$lib/scripts' import { Button, Drawer, DrawerContent } from './common' diff --git a/frontend/src/lib/components/Select.svelte b/frontend/src/lib/components/Select.svelte deleted file mode 100644 index 0afdf2cc32..0000000000 --- a/frontend/src/lib/components/Select.svelte +++ /dev/null @@ -1,271 +0,0 @@ - - - { - if (!open || !processedItems?.length) return - if (e.key === 'ArrowUp' && keyArrowPos !== undefined && processedItems.length > 0) { - keyArrowPos = keyArrowPos <= 0 ? undefined : keyArrowPos - 1 - } else if (e.key === 'ArrowDown') { - if (keyArrowPos === undefined) { - keyArrowPos = 0 - } else { - keyArrowPos = Math.min(processedItems.length - 1, keyArrowPos + 1) - } - } else if (e.key === 'Enter' && keyArrowPos !== undefined && processedItems?.[keyArrowPos]) { - setValue(processedItems[keyArrowPos]) - } else { - keyArrowPos = undefined - return - } - e.preventDefault() - }} -/> - -
(open = false) }} - onpointerdown={() => onFocus?.()} - onfocus={() => onFocus?.()} - onblur={() => onBlur?.()} -> - {#if loading} -
- -
- {:else if clearable && !disabled && value} -
- -
- {:else if RightIcon} -
- -
- {/if} - - filterText, (v) => (filterText = v)} - placeholder={loading ? 'Loading...' : (valueEntry?.label ?? placeholder)} - style={containerStyle} - class={twMerge( - '!bg-surface text-ellipsis', - open ? '' : 'cursor-pointer', - valueEntry && !loading ? '!placeholder-primary' : '', - (clearable || RightIcon) && !disabled && value ? '!pr-8' : '', - inputClass ?? '' - )} - autocomplete="off" - onpointerdown={() => (open = true)} - bind:this={inputEl} - /> - - - {#if open && !disabled} -
- {#if processedItems?.length === 0} -
No items
- {/if} - {#each processedItems ?? [] as item, itemIndex} - {#if (item.__select_group && itemIndex === 0) || processedItems?.[itemIndex - 1]?.__select_group !== item.__select_group} -
- {item.__select_group} -
- {/if} - - {/each} -
- {/if} -
-
diff --git a/frontend/src/lib/components/ServiceLogsInner.svelte b/frontend/src/lib/components/ServiceLogsInner.svelte index 4504d4e69d..b8b233bf6f 100644 --- a/frontend/src/lib/components/ServiceLogsInner.svelte +++ b/frontend/src/lib/components/ServiceLogsInner.svelte @@ -11,14 +11,13 @@ import { sendUserToast } from '$lib/toast' import { onDestroy, tick, untrack } from 'svelte' import { Loader2 } from 'lucide-svelte' - import { copyToClipboard, truncateRev } from '$lib/utils' + import { copyToClipboard, scroll_into_view_if_needed_polyfill, truncateRev } from '$lib/utils' import LogSnippetViewer from './LogSnippetViewer.svelte' import { Button, Drawer, DrawerContent } from './common' import ClipboardCopy from 'lucide-svelte/icons/clipboard-copy' import AnsiUp from 'ansi_up' - import { scroll_into_view_if_needed_polyfill } from './multiselect/utils' import SplitPanesOrColumnOnMobile from './splitPanes/SplitPanesOrColumnOnMobile.svelte' - import Select from './Select.svelte' + import Select from './select/Select.svelte' import { goto } from '$lib/navigation' import { page } from '$app/stores' diff --git a/frontend/src/lib/components/ShareModal.svelte b/frontend/src/lib/components/ShareModal.svelte index eac1b0fefd..a8eeeeb9d1 100644 --- a/frontend/src/lib/components/ShareModal.svelte +++ b/frontend/src/lib/components/ShareModal.svelte @@ -13,7 +13,8 @@ import { isOwner } from '$lib/utils' import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte' import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte' - import Select from './Select.svelte' + import Select from './select/Select.svelte' + import { safeSelectItems } from './select/utils.svelte' const dispatch = createEventDispatcher() @@ -138,9 +139,9 @@
{#key ownerKind} !worker_tags?.includes(x)) - .map((x) => ({ value: x, label: x }))} + items={safeSelectItems( + [...(customTags ?? []), ...createdTags, ...defaultTags, ...nativeTags].filter( + (x) => !worker_tags?.includes(x) + ) + )} {disabled} bind:value={newTag} onFocus={() => dispatch('focus')} diff --git a/frontend/src/lib/components/TeamSelector.svelte b/frontend/src/lib/components/TeamSelector.svelte index 638c30d22f..899314bb31 100644 --- a/frontend/src/lib/components/TeamSelector.svelte +++ b/frontend/src/lib/components/TeamSelector.svelte @@ -2,7 +2,7 @@ import { workspaceStore } from '$lib/stores' import { RefreshCcw } from 'lucide-svelte' import { WorkspaceService } from '$lib/gen' - import Select from './Select.svelte' + import Select from './select/Select.svelte' import { onMount } from 'svelte' interface TeamItem { diff --git a/frontend/src/lib/components/WorkerGroup.svelte b/frontend/src/lib/components/WorkerGroup.svelte index 2dd2bd08ef..403995db2c 100644 --- a/frontend/src/lib/components/WorkerGroup.svelte +++ b/frontend/src/lib/components/WorkerGroup.svelte @@ -1,7 +1,6 @@ -{#each Object.keys(components['multiselectcomponent'].initialData.configuration) as key (key)} +{#each Object.keys(components['multiselectcomponentv2'].initialData.configuration) as key (key)} {/each} @@ -193,74 +160,33 @@
{ + class={twMerge('w-full', resolvedConfig.allowOverflow ? '' : 'absolute inset-0')} + onpointerdown={(e) => { $selectedComponent = [id] - - if (!e.shiftKey) { - e.stopPropagation() - } + if (!e.shiftKey) e.stopPropagation() selectedComponent.set([id]) }} - use:floatingRef - bind:clientWidth={w} > - {#if !selectedItems || Array.isArray(selectedItems)} - { - if (event?.detail?.type === 'removeAll') { - outputs?.result.set([]) - setContextValue([]) - } else { - setResultsFromSelectedItems() + { + customItems.push(item) + selectedItems.push(item) + customItems = customItems + selectedItems = selectedItems } - }} - on:open={() => { - $selectedComponent = [id] - open = true - }} - on:close={() => { - open = false - }} - let:option - > - - -
{ - let newe = new MouseEvent('mouseup') - e.target?.['parentElement']?.dispatchEvent(newe) - }} - > - {typeof option == 'object' ? (option?.label ?? 'NO_LABEL') : option} -
-
- - - - {:else} - Value {selectedItems} is not an array - {/if} + : undefined} + onOpen={() => ($selectedComponent = [id])} + />
diff --git a/frontend/src/lib/components/apps/components/inputs/AppSelect.svelte b/frontend/src/lib/components/apps/components/inputs/AppSelect.svelte index 5c9fff9c35..4e7d7dc8fc 100644 --- a/frontend/src/lib/components/apps/components/inputs/AppSelect.svelte +++ b/frontend/src/lib/components/apps/components/inputs/AppSelect.svelte @@ -19,7 +19,7 @@ import { Bug } from 'lucide-svelte' import Popover from '$lib/components/Popover.svelte' import ResolveStyle from '../helpers/ResolveStyle.svelte' - import Select from '$lib/components/Select.svelte' + import Select from '$lib/components/select/Select.svelte' interface Props { id: string diff --git a/frontend/src/lib/components/apps/editor/AppEditor.svelte b/frontend/src/lib/components/apps/editor/AppEditor.svelte index d7555b8460..75db79da69 100644 --- a/frontend/src/lib/components/apps/editor/AppEditor.svelte +++ b/frontend/src/lib/components/apps/editor/AppEditor.svelte @@ -1256,7 +1256,7 @@ {:else}
- Select a component to see the settings for it + Select a component to see the settings for it
{/if} diff --git a/frontend/src/lib/components/apps/editor/GridEditor.svelte b/frontend/src/lib/components/apps/editor/GridEditor.svelte index 01a725683b..faa5a321e8 100644 --- a/frontend/src/lib/components/apps/editor/GridEditor.svelte +++ b/frontend/src/lib/components/apps/editor/GridEditor.svelte @@ -127,6 +127,7 @@ } +
({ id, forceStyle: true, forceClass: true })) }, ...Object.entries(ccomponents) - .filter(([key]) => key !== 'quillcomponent') + .filter(([key]) => !['quillcomponent', ...Object.keys(DEPRECATED_COMPONENTS)].includes(key)) .map(([type, { name, icon, customCss }]) => ({ type: type as keyof typeof components, name, diff --git a/frontend/src/lib/components/apps/editor/componentsPanel/cssUtils.ts b/frontend/src/lib/components/apps/editor/componentsPanel/cssUtils.ts index 4634959ac0..8545d0627c 100644 --- a/frontend/src/lib/components/apps/editor/componentsPanel/cssUtils.ts +++ b/frontend/src/lib/components/apps/editor/componentsPanel/cssUtils.ts @@ -676,8 +676,8 @@ export const customisationByComponent: Customisation[] = [ comment: 'buttons to remove a single or all selected options at once' }, { - selector: '.multiselect > input[autocomplete]', - comment: 'input inside the top-level wrapper div' + selector: '.multiselect.dropdown', + comment: 'dropdown container' }, { selector: '.multiselect > ul.options', @@ -686,23 +686,6 @@ export const customisationByComponent: Customisation[] = [ { selector: '.multiselect > ul.options > li', comment: 'dropdown list items' - }, - { - selector: '.multiselect > ul.options > li.selected', - comment: 'selected options in the dropdown list' - }, - { - selector: '.multiselect > ul.options > li:not(.selected):hover', - comment: 'unselected but hovered options in the dropdown list' - }, - { - selector: '.multiselect > ul.options > li.active', - comment: - 'active item, navigated to with up/down arrow keys and ready to be selected by pressing enter' - }, - { - selector: '.multiselect > ul.options > li.disabled', - comment: 'options with disabled key set to true' } ], variables: [ diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/EventHandlerItem.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/EventHandlerItem.svelte index 83b05f5fd0..37697dec9d 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/EventHandlerItem.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/EventHandlerItem.svelte @@ -1,11 +1,16 @@ {#if condition} - + {@render children?.()} {:else} - + {@render children?.()} {/if} diff --git a/frontend/src/lib/components/multiselect/MultiSelect.svelte b/frontend/src/lib/components/multiselect/MultiSelect.svelte deleted file mode 100644 index 1e4f00c66d..0000000000 --- a/frontend/src/lib/components/multiselect/MultiSelect.svelte +++ /dev/null @@ -1,828 +0,0 @@ - - - - - - - diff --git a/frontend/src/lib/components/multiselect/MultiSelectWrapper.svelte b/frontend/src/lib/components/multiselect/MultiSelectWrapper.svelte deleted file mode 100644 index 685c9b0801..0000000000 --- a/frontend/src/lib/components/multiselect/MultiSelectWrapper.svelte +++ /dev/null @@ -1,130 +0,0 @@ - - - - -
- {#if !value || Array.isArray(value)} -
- [...(value ?? [])], - (newVal) => { - if (!deepEqual(value, newVal)) { - value = newVal - } - } - } - {placeholder} - options={items} - on:close={() => { - open = false - }} - on:open={() => { - open = true - }} - let:option - disableRemoveAll - > -
{ - e.stopPropagation() - }} - onpointerdown={(e) => { - e.stopPropagation() - let newe = new MouseEvent('mouseup') - e.target?.['parentElement']?.dispatchEvent(newe) - }} - aria-selected={value?.includes(option)} - > - {option} -
-
-
- - - - {:else} - Value {value} is not an array - {/if} -
diff --git a/frontend/src/lib/components/multiselect/types.ts b/frontend/src/lib/components/multiselect/types.ts deleted file mode 100644 index d3dc4d7cac..0000000000 --- a/frontend/src/lib/components/multiselect/types.ts +++ /dev/null @@ -1,47 +0,0 @@ -export type Option = string | number | ObjectOption - -// single CSS string or an object with keys 'option' and 'selected', each a string, -// which only apply to the dropdown list and list of selected options, respectively -export type OptionStyle = string | { option: string; selected: string } - -export type ObjectOption = { - label: string | number // user-displayed text - value?: unknown // associated value, can be anything incl. objects (defaults to label if undefined) - title?: string // on-hover tooltip - disabled?: boolean // make this option unselectable - preselected?: boolean // make this option selected on page load (before any user interaction) - disabledTitle?: string // override the default disabledTitle = 'This option is disabled' - selectedTitle?: string // tooltip to display when this option is selected and hovered - style?: OptionStyle - [key: string]: unknown // allow any other keys users might want -} - -export type DispatchEvents = { - add: { option: T } - create: { option: T } - remove: { option: T } - removeAll: { options: T[] } - change: { - option?: T - options?: T[] - type: 'add' | 'remove' | 'removeAll' - } - open: { event: Event } - close: { event: Event } -} - -export type MultiSelectEvents = { - [key in keyof DispatchEvents]: CustomEvent -} & { - blur: FocusEvent - click: MouseEvent - focus: FocusEvent - keydown: KeyboardEvent - keyup: KeyboardEvent - mouseenter: MouseEvent - mouseleave: MouseEvent - touchcancel: TouchEvent - touchend: TouchEvent - touchmove: TouchEvent - touchstart: TouchEvent -} diff --git a/frontend/src/lib/components/multiselect/utils.ts b/frontend/src/lib/components/multiselect/utils.ts deleted file mode 100644 index a9bcfcaa69..0000000000 --- a/frontend/src/lib/components/multiselect/utils.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { Option, OptionStyle } from './types' - -// get the label key from an option object or the option itself if it's a string or number -export const get_label = (opt: Option) => { - if (opt instanceof Object) { - if (opt.label === undefined) { - console.error(`MultiSelect option ${JSON.stringify(opt)} is an object but has no label key`) - } - return opt.label - } - return `${opt}` -} - -// this function is used extract CSS strings from a {selected, option} style object to be used in the style attribute of the option -// if the style is a string, it will be returned as is -export function get_style( - option: { style?: OptionStyle; [key: string]: unknown } | string | number, - key: 'selected' | 'option' | null = null -) { - let css_str = `` - if (![`selected`, `option`, null].includes(key)) { - console.error(`MultiSelect: Invalid key=${key} for get_style`) - } - if (typeof option == `object` && option.style) { - if (typeof option.style == `string`) { - css_str = option.style - } - if (typeof option.style == `object`) { - if (key && key in option.style) return option.style[key] ?? `` - else { - console.error(`Invalid style object for option=${JSON.stringify(option)}`) - } - } - } - // ensure css_str ends with a semicolon - if (css_str.trim() && !css_str.trim().endsWith(`;`)) css_str += `;` - return css_str -} - -// Firefox lacks support for scrollIntoViewIfNeeded (https://caniuse.com/scrollintoviewifneeded). -// See https://github.com/janosh/svelte-multiselect/issues/87 -// Polyfill copied from -// https://github.com/nuxodin/lazyfill/blob/a8e63/polyfills/Element/prototype/scrollIntoViewIfNeeded.js -// exported for testing -export function scroll_into_view_if_needed_polyfill(elem: Element, centerIfNeeded: boolean = true) { - const observer = new IntersectionObserver(function ([entry]) { - const ratio = entry.intersectionRatio - if (ratio < 1) { - const place = ratio <= 0 && centerIfNeeded ? `center` : `nearest` - elem.scrollIntoView({ - block: place, - inline: place - }) - } - observer.disconnect() - }, { - root: null, // or specify a scrolling parent if needed - rootMargin: '0px 1000px', // Essentially making horizontal checks irrelevant - threshold: 0.1 // Adjust threshold to control when observer should trigger -}) - observer.observe(elem) - - return observer // return for testing -} diff --git a/frontend/src/lib/components/runs/RunsFilter.svelte b/frontend/src/lib/components/runs/RunsFilter.svelte index 6fd0b0ed03..77ab068bfe 100644 --- a/frontend/src/lib/components/runs/RunsFilter.svelte +++ b/frontend/src/lib/components/runs/RunsFilter.svelte @@ -12,7 +12,8 @@ import { createEventDispatcher, untrack } from 'svelte' import ToggleButtonMore from '../common/toggleButton-v2/ToggleButtonMore.svelte' import Popover from '$lib/components/meltComponents/Popover.svelte' - import Select from '../Select.svelte' + import Select from '../select/Select.svelte' + import { safeSelectItems } from '../select/utils.svelte' interface Props { // Filters @@ -174,7 +175,7 @@
User ({ label: p, value: p }))} + items={safeSelectItems(folders)} bind:value={() => folder ?? undefined, (v) => (folder = v ?? null)} clearable onClear={() => ((folder = null), dispatch('reset'))} @@ -203,7 +204,7 @@
Path ({ label: p, value: p }))} + items={safeSelectItems(usernames)} bind:value={() => user ?? undefined, (v) => (user = v ?? null)} clearable onClear={() => ((user = null), dispatch('reset'))} @@ -575,7 +576,7 @@
+ {/if} + {/snippet} + +
diff --git a/frontend/src/lib/components/select/Select.svelte b/frontend/src/lib/components/select/Select.svelte new file mode 100644 index 0000000000..ec0a4dcc47 --- /dev/null +++ b/frontend/src/lib/components/select/Select.svelte @@ -0,0 +1,156 @@ + + +
(open = false) }} + onpointerdown={() => onFocus?.()} + onfocus={() => onFocus?.()} + onblur={() => onBlur?.()} +> + {#if loading} +
+ +
+ {:else if clearable && !disabled && value} +
+ +
+ {:else if RightIcon} +
+ +
+ {/if} + + filterText, (v) => (filterText = v)} + placeholder={loading ? 'Loading...' : (valueEntry?.label ?? placeholder)} + style={containerStyle} + class={twMerge( + '!bg-surface text-ellipsis', + open ? '' : 'cursor-pointer', + valueEntry && !loading ? '!placeholder-primary' : '', + (clearable || RightIcon) && !disabled && value ? '!pr-8' : '', + inputClass ?? '' + )} + autocomplete="off" + onpointerdown={() => (open = true)} + bind:this={inputEl} + /> + inputEl!.getBoundingClientRect())} + {listAutoWidth} + {noItemsMsg} + /> +
diff --git a/frontend/src/lib/components/select/SelectDropdown.svelte b/frontend/src/lib/components/select/SelectDropdown.svelte new file mode 100644 index 0000000000..7780828a70 --- /dev/null +++ b/frontend/src/lib/components/select/SelectDropdown.svelte @@ -0,0 +1,147 @@ + + + { + if (!open || !processedItems?.length) return + if (e.key === 'ArrowUp' && keyArrowPos !== undefined && processedItems.length > 0) { + keyArrowPos = keyArrowPos <= 0 ? undefined : keyArrowPos - 1 + } else if (e.key === 'ArrowDown') { + if (keyArrowPos === undefined) { + keyArrowPos = 0 + } else { + keyArrowPos = Math.min(processedItems.length - 1, keyArrowPos + 1) + } + } else if (e.key === 'Enter' && keyArrowPos !== undefined && processedItems?.[keyArrowPos]) { + onSelectValue(processedItems[keyArrowPos]) + } else { + keyArrowPos = undefined + return + } + e.preventDefault() + }} +/> + + + {#if open && !disabled} +
+ {@render header?.()} + {#if processedItems?.length === 0} +
{noItemsMsg}
+ {/if} +
    + {#each processedItems ?? [] as item, itemIndex} + {#if (item.__select_group && itemIndex === 0) || processedItems?.[itemIndex - 1]?.__select_group !== item.__select_group} +
  • + {item.__select_group} +
  • + {/if} +
  • + +
  • + {/each} +
+
+ {/if} +
diff --git a/frontend/src/lib/components/select/utils.svelte.ts b/frontend/src/lib/components/select/utils.svelte.ts new file mode 100644 index 0000000000..f3d43b40c1 --- /dev/null +++ b/frontend/src/lib/components/select/utils.svelte.ts @@ -0,0 +1,72 @@ +export function processItems({ + items, + filterText, + groupBy, + sortBy, + onCreateItem, + createText +}: { + items?: Item[] + filterText?: string + groupBy?: (item: Item) => string + sortBy?: (a: Item, b: Item) => number + onCreateItem?: (value: string) => void + createText?: string +}): ProcessedItem[] { + let items2 = + items?.map((item) => ({ + ...item, + label: getLabel(item) + })) ?? [] + if (groupBy) { + items2 = + items2?.map((item) => ({ + ...item, + __select_group: groupBy(item) + })) ?? [] + } + if (sortBy) { + items2 = items2?.sort(sortBy) + } + if (onCreateItem && filterText && !items2.some((item) => item.label === filterText)) { + items2.push({ + label: createText ?? `Add new: "${filterText}"`, + value: filterText, + __is_create: true + } as any) + } + return items2 +} + +export type ProcessedItem = { + __select_group?: string + __is_create?: true + label: string + value: T +} + +export function getLabel(item: { label?: string; value: T } | undefined): string { + if (!item) return '' + if (item.label) return item.label + if (typeof item.value === 'string') return item.value + if (typeof item.value == 'number' || typeof item.value == 'boolean') return item.value.toString() + + return JSON.stringify(item.value) +} + +export function safeSelectItems( + list: (T | { value: T; label?: string } | undefined | null)[] | undefined | null +): { value: T; label?: string }[] { + if (!list) return [] + return list + .filter((item) => item !== undefined && item !== null) + .map((item) => { + if (typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean') { + return { value: item } + } + if (typeof item === 'object' && 'value' in item) { + return item + } + return { value: null as any, label: 'UNKNOWN_ITEM' } + }) +} diff --git a/frontend/src/lib/components/settings/CreateToken.svelte b/frontend/src/lib/components/settings/CreateToken.svelte index dd25ca2da9..70c60e2aca 100644 --- a/frontend/src/lib/components/settings/CreateToken.svelte +++ b/frontend/src/lib/components/settings/CreateToken.svelte @@ -3,7 +3,6 @@ import { Button } from '../common' import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte' import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte' - import MultiSelectWrapper from '../multiselect/MultiSelectWrapper.svelte' import TriggerableByAI from '../TriggerableByAI.svelte' import Toggle from '../Toggle.svelte' import { UserService, type NewToken } from '$lib/gen' @@ -39,7 +38,7 @@ let newMcpScope = $state('favorites') let loadingApps = $state(false) let errorFetchApps = $state(false) - let allApps = $state([]) + // let allApps = $state([]) function handleCopyClick() { copyToClipboard(newToken ?? '') @@ -205,7 +204,7 @@ {:else if errorFetchApps}
Error fetching apps
{:else} - + {/if}
diff --git a/frontend/src/lib/components/settings/TokensTable.svelte b/frontend/src/lib/components/settings/TokensTable.svelte index 9601b92f67..d66e6c2df1 100644 --- a/frontend/src/lib/components/settings/TokensTable.svelte +++ b/frontend/src/lib/components/settings/TokensTable.svelte @@ -12,8 +12,9 @@ import Toggle from '../Toggle.svelte' import ClipboardPanel from '../details/ClipboardPanel.svelte' import { sendUserToast } from '$lib/toast' - import MultiSelectWrapper from '../multiselect/MultiSelectWrapper.svelte' import TriggerableByAI from '../TriggerableByAI.svelte' + import MultiSelect from '../select/MultiSelect.svelte' + import { safeSelectItems } from '../select/utils.svelte' // --- Props --- interface Props { @@ -306,10 +307,11 @@ {:else if errorFetchApps}
Error fetching apps
{:else} - {/if}
@@ -319,7 +321,7 @@ +
{#if !mcpCreationMode} @@ -339,7 +341,7 @@ Expires In (optional) - diff --git a/frontend/src/lib/components/triggers/gcp/GcpTriggerEditorConfigSection.svelte b/frontend/src/lib/components/triggers/gcp/GcpTriggerEditorConfigSection.svelte index 83aa8633ce..60751a4595 100644 --- a/frontend/src/lib/components/triggers/gcp/GcpTriggerEditorConfigSection.svelte +++ b/frontend/src/lib/components/triggers/gcp/GcpTriggerEditorConfigSection.svelte @@ -23,7 +23,8 @@ import { RefreshCw } from 'lucide-svelte' import Alert from '$lib/components/common/alert/Alert.svelte' import TestingBadge from '../testingBadge.svelte' - import Select from '$lib/components/Select.svelte' + import Select from '$lib/components/select/Select.svelte' + import { safeSelectItems } from '$lib/components/select/utils.svelte' let topic_items: string[] = $state([]) let subscription_items: string[] = $state([]) @@ -176,7 +177,7 @@ loadAllSubscriptionFromGooglePubSubTopic() } } - items={topic_items.map((value) => ({ value }))} + items={safeSelectItems(topic_items)} placeholder="Choose a topic" />