Files
windmill/frontend/src/lib/components/select/Select.svelte
T
wendrulandwindmill-internal-app[bot] a1cd02d7f8 feat: restriction rulesets for workspaces (#7791)
* Add frontend for the workspace proctection rules

* api to add and update workspace protection rules

* Fix bypasser selection

* Fix Select bug on refocus

* Change rulesets to use bitflags

* Messages for protection rules errors

* claude remove ui for rules

* Hide edit buttons when rule

* No edit refactor pt1

* Update edit buttons to be disabled when rule is active

* Merge deploy ui and rulsets in one tab

* Remove not cleaned line in migration

* multiple fixes

* Remove old protection rule logic

* Add prrotection rule for deploying through Merge UI

* Add Alert on legacy Deploy UI

* Add backend enforcing of workspace rules

* Finish backend blocking on rulsets

* Last changes to api ruleset blocks

* Prepare sqlx

* Remove unused import and argument

* Update SQLx metadata

* fix npm run check

* Re trigger CI

---------

Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-02-05 18:02:41 +00:00

210 lines
5.3 KiB
Svelte

<script
lang="ts"
generics="Item extends { label?: string; value: any; subtitle?: string; disabled?: boolean }"
>
import { clickOutside } from '$lib/utils'
import { twMerge } from 'tailwind-merge'
import CloseButton from '../common/CloseButton.svelte'
import { Loader2 } from 'lucide-svelte'
import { untrack, type Snippet } from 'svelte'
import { getLabel, processItems, type ProcessedItem } from './utils.svelte'
import SelectDropdown from './SelectDropdown.svelte'
import { deepEqual } from 'fast-equals'
import {
inputBaseClass,
inputBorderClass,
inputSizeClasses
} from '../text_input/TextInput.svelte'
import { ButtonType } from '../common/button/model'
type Value = Item['value']
let {
items,
placeholder = 'Please select',
value = $bindable(),
filterText = $bindable(''),
class: className = '',
clearable = false,
listAutoWidth = true,
disabled: _disabled = false,
containerStyle = '',
inputClass = '',
disablePortal = false,
loading = false,
error = false,
autofocus,
RightIcon,
createText,
noItemsMsg,
open = $bindable(false),
id,
itemLabelWrapperClasses,
itemButtonWrapperClasses,
size = 'md',
showPlaceholderOnOpen = false,
transformInputSelectedText,
groupBy,
sortBy,
onFocus,
onBlur,
onClear,
onCreateItem,
startSnippet,
endSnippet,
bottomSnippet
}: {
items?: Item[]
value: Value | undefined
placeholder?: string
class?: string
clearable?: boolean
filterText?: string
disabled?: boolean
listAutoWidth?: boolean
containerStyle?: string
inputClass?: string
disablePortal?: boolean
loading?: boolean
error?: boolean
autofocus?: boolean
RightIcon?: any
createText?: string
noItemsMsg?: string
open?: boolean
id?: string
itemLabelWrapperClasses?: string
itemButtonWrapperClasses?: string
size?: 'sm' | 'md' | 'lg'
showPlaceholderOnOpen?: boolean
transformInputSelectedText?: (text: string) => string
groupBy?: (item: Item) => string
sortBy?: (a: Item, b: Item) => number
onFocus?: () => void
onBlur?: () => void
onClear?: () => void
onCreateItem?: (value: string) => void
startSnippet?: Snippet<[{ item: ProcessedItem<Value>; close: () => void }]>
endSnippet?: Snippet<[{ item: ProcessedItem<Value>; close: () => void }]>
bottomSnippet?: Snippet<[{ close: () => void }]>
} = $props()
let disabled = $derived(_disabled || (loading && !value))
let iconSize = $derived(ButtonType.UnifiedIconSizes[size])
let inputEl: HTMLInputElement | undefined = $state()
let processedItems: ProcessedItem<Value>[] = $derived.by(() => {
let args = { items, createText, filterText, groupBy, onCreateItem, sortBy }
return untrack(() => processItems(args))
})
$effect(() => {
if (filterText) open = true
})
$effect(() => {
if (!open) filterText = ''
})
let valueEntry = $derived(value && processedItems?.find((item) => deepEqual(item.value, value)))
function setValue(item: ProcessedItem<Value>) {
if (item.__is_create && onCreateItem) {
onCreateItem(item.value)
} else {
value = item.value
}
filterText = ''
open = false
}
function clearValue() {
filterText = ''
if (onClear) onClear()
else value = undefined
}
let inputText = $derived.by(() => {
let text = valueEntry?.label ?? getLabel({ value }) ?? ''
return transformInputSelectedText?.(text) ?? text
})
</script>
<div
class={`relative ${className}`}
use:clickOutside={{ onClickOutside: () => (open = false) }}
onpointerdown={() => onFocus?.()}
onfocus={() => onFocus?.()}
onblur={() => onBlur?.()}
>
{#if loading}
<div class="absolute z-10 right-2 h-full flex items-center">
<Loader2 size={iconSize} class="animate-spin" />
</div>
{:else if clearable && !disabled && value}
<div class="absolute z-10 right-2 h-full flex items-center">
<CloseButton
class="bg-transparent text-secondary hover:text-primary"
noBg
small
on:close={clearValue}
/>
</div>
{:else if RightIcon}
<div class="absolute z-10 right-2 h-full flex items-center">
<RightIcon size={iconSize} class="text-secondary" />
</div>
{/if}
<!-- svelte-ignore a11y_autofocus -->
<input
{autofocus}
{disabled}
type="text"
bind:value={() => (open ? filterText : inputText), (v) => open && (filterText = v)}
placeholder={loading && !value
? 'Loading...'
: value && !showPlaceholderOnOpen
? inputText
: placeholder}
style={containerStyle}
class={twMerge(
inputBaseClass,
inputSizeClasses[size],
ButtonType.UnifiedHeightClasses[size],
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',
(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
}}
onpointerdown={() => (open = true)}
bind:this={inputEl}
{id}
/>
<SelectDropdown
{disablePortal}
onSelectValue={setValue}
{open}
{processedItems}
{value}
{disabled}
{filterText}
getInputRect={inputEl && (() => inputEl!.getBoundingClientRect())}
{listAutoWidth}
{noItemsMsg}
{itemLabelWrapperClasses}
{itemButtonWrapperClasses}
{startSnippet}
{endSnippet}
{bottomSnippet}
/>
</div>