mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-22 00:01:34 +00:00
484fdfa2bc
* Improve workers page * Update group config drawer * improve dirty workergroup config * Make layout reactive * fix section animation * prevent opening dropdown while clicking New group config * migrate workers page to svelte 5 * Open drawer upon adding a worker group * nit critical alert table * improve queue metrics drawer * improve agent worker drawer * harmonize copy icon * improve agent worker doc * improve layout * Improve autoscaling event list * Improve tags managment * Remove default tags * fix npm check * Add info for agent workers * improve agent worker jwt token creation * Improve token display * nit * improve tag display * create EE component * nit * harmonize tag overflow * handle permission better * improve env var presets * handle permission for config * nit alerts * nit * Improve custom tag creation in tag select * optimistic tag addition * nit * nit * fix typo * improve workers table * Group config tags * show mismatch * fix typo * optimistic update when adding tag * do not allow to create tag when picking a tag to watch in alerts
200 lines
4.9 KiB
Svelte
200 lines
4.9 KiB
Svelte
<script lang="ts" generics="Item extends { label?: string; value: any; }">
|
|
import { clickOutside, reorder } from '$lib/utils'
|
|
import { untrack } from 'svelte'
|
|
import { processItems, type ProcessedItem } from './utils.svelte'
|
|
import SelectDropdown from './SelectDropdown.svelte'
|
|
import CloseButton from '../common/CloseButton.svelte'
|
|
import DraggableTags from './DraggableTags.svelte'
|
|
import { Search } from 'lucide-svelte'
|
|
import { twMerge } from 'tailwind-merge'
|
|
import TextInput, {
|
|
inputBaseClass,
|
|
inputBorderClass,
|
|
inputSizeClasses
|
|
} from '../text_input/TextInput.svelte'
|
|
|
|
type Value = Item['value']
|
|
|
|
let {
|
|
items,
|
|
placeholder = 'Select items',
|
|
value = $bindable(),
|
|
class: className = '',
|
|
style,
|
|
listAutoWidth = true,
|
|
disabled = false,
|
|
disablePortal = false,
|
|
createText,
|
|
reorderable = true,
|
|
noItemsMsg,
|
|
selectedUlClass = '',
|
|
placeholderClass = '',
|
|
allowClear = true,
|
|
hideMainClearBtn = false,
|
|
size = 'md',
|
|
id,
|
|
error = false,
|
|
onOpen,
|
|
groupBy,
|
|
sortBy,
|
|
onCreateItem
|
|
}: {
|
|
items?: Item[]
|
|
value: Value[]
|
|
placeholder?: string
|
|
class?: string
|
|
style?: string
|
|
filterText?: string
|
|
disabled?: boolean
|
|
listAutoWidth?: boolean
|
|
containerStyle?: string
|
|
inputClass?: string
|
|
disablePortal?: boolean
|
|
createText?: string
|
|
reorderable?: boolean
|
|
noItemsMsg?: string
|
|
selectedUlClass?: string
|
|
placeholderClass?: string
|
|
allowClear?: boolean
|
|
hideMainClearBtn?: boolean
|
|
size?: keyof typeof inputSizeClasses
|
|
id?: string
|
|
error?: boolean
|
|
groupBy?: (item: Item) => string
|
|
sortBy?: (a: Item, b: Item) => number
|
|
onOpen?: () => void
|
|
onClear?: () => void
|
|
onCreateItem?: (value: string) => void
|
|
} = $props()
|
|
|
|
let filterText = $state<string>('')
|
|
let open = $state<boolean>(false)
|
|
let wrapperEl: HTMLDivElement | undefined = $state()
|
|
let searchInputEl: TextInput | undefined = $state()
|
|
|
|
$effect(() => searchInputEl?.focus())
|
|
|
|
let processedItems: ProcessedItem<Value>[] = $derived.by(() => {
|
|
let args = { items, createText, filterText, groupBy, onCreateItem, sortBy }
|
|
return untrack(() => processItems(args))
|
|
})
|
|
|
|
$effect(() => {
|
|
if (!open) filterText = ''
|
|
})
|
|
$effect(() => {
|
|
open && untrack(() => onOpen?.())
|
|
})
|
|
|
|
let valueEntry = $derived(
|
|
value.map((v) => processedItems.find((item) => item.value === v) ?? { value: v, label: v })
|
|
)
|
|
|
|
function onAddValue(item: ProcessedItem<Value>) {
|
|
if (item.__is_create && onCreateItem) {
|
|
onCreateItem(item.value)
|
|
} else {
|
|
value = [...value, item.value]
|
|
}
|
|
}
|
|
function onRemoveValue(item: ProcessedItem<Value>) {
|
|
value = value.filter((v) => v !== item.value)
|
|
}
|
|
|
|
function clearValue() {
|
|
filterText = ''
|
|
value = []
|
|
}
|
|
|
|
export function getFilteredInputText() {
|
|
return filterText
|
|
}
|
|
</script>
|
|
|
|
<div
|
|
bind:this={wrapperEl}
|
|
class={twMerge(
|
|
'flex items-center flex-wrap relative',
|
|
inputBaseClass,
|
|
inputSizeClasses[size],
|
|
inputBorderClass({ forceFocus: open && !disabled, error }),
|
|
disabled ? 'pointer-events-none' : '',
|
|
open && !disabled ? 'open' : '',
|
|
disabled ? 'disabled' : '',
|
|
className
|
|
)}
|
|
{style}
|
|
onpointerup={() => (open = true)}
|
|
use:clickOutside={{ onClickOutside: () => (open = false) }}
|
|
{id}
|
|
>
|
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
|
|
{#if value.length === 0}
|
|
<span class={twMerge('text-xs h-full flex items-center flex-1 text-hint', placeholderClass)}>
|
|
{placeholder}
|
|
</span>
|
|
{:else}
|
|
<ul
|
|
class={twMerge(
|
|
'overflow-clip overflow-x-hidden h-full cursor-pointer items-center flex flex-wrap gap-1 py-0.5 flex-1 text-primary',
|
|
selectedUlClass
|
|
)}
|
|
role="list"
|
|
>
|
|
<DraggableTags
|
|
items={valueEntry}
|
|
{allowClear}
|
|
onRemove={onRemoveValue}
|
|
onReorder={reorderable
|
|
? (oldIdx, newIdx) => (value = reorder(value, oldIdx, newIdx))
|
|
: undefined}
|
|
/>
|
|
</ul>
|
|
{/if}
|
|
{#if allowClear && !hideMainClearBtn && !!value?.length}
|
|
<CloseButton
|
|
noBg
|
|
class="mr-1 remove-all bg-transparent text-hint"
|
|
small
|
|
on:close={(e) => (clearValue(), e.stopPropagation())}
|
|
/>
|
|
{/if}
|
|
<SelectDropdown
|
|
{disablePortal}
|
|
onSelectValue={onAddValue}
|
|
{open}
|
|
processedItems={processedItems.filter((item) => !value.includes(item.value))}
|
|
value={undefined}
|
|
{disabled}
|
|
{filterText}
|
|
getInputRect={wrapperEl && (() => wrapperEl!.getBoundingClientRect())}
|
|
{listAutoWidth}
|
|
{noItemsMsg}
|
|
class={twMerge(
|
|
'multiselect dropdown',
|
|
open && !disabled ? 'open' : '',
|
|
disabled ? 'disabled' : ''
|
|
)}
|
|
ulClass="options"
|
|
>
|
|
{#snippet header()}
|
|
{#if processedItems.length - value.length > 0 || onCreateItem}
|
|
<div class="mx-2 mb-1 mt-2 flex items-center relative">
|
|
<TextInput
|
|
bind:this={searchInputEl}
|
|
bind:value={filterText}
|
|
inputProps={{
|
|
onblur: (e) => (e.preventDefault(), searchInputEl?.focus()),
|
|
placeholder: 'Search...'
|
|
}}
|
|
class="!pr-7 !bg-surface"
|
|
/>
|
|
<Search size={16} class="absolute right-2 text-primary" />
|
|
</div>
|
|
{/if}
|
|
{/snippet}
|
|
</SelectDropdown>
|
|
</div>
|