Files
windmill/frontend/src/lib/components/select/GenericDropdown.svelte
T
Diego ImbertandClaude Opus 5.5 4fd7d62bd0 feat: rework the db manager: native grid, tabs, sql editor, joined columns (#11340)
* feat: replace ag-grid in the db manager table viewer with a native grid

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: add user-managed data, diagram and sql editor tabs to the db manager

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: add schema autocomplete to the db manager sql editor and polish its layout

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: add joined foreign key columns and draggable tabs to the db manager

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: move db manager tabs on drop instead of mid-drag

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: open followed foreign keys in a new db manager tab

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: scroll large tables, drop stale joins and return to the last tab on close

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: tolerate the db manager tabs going away while switching data table

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: keep saved joined columns while metadata loads, test joins in every dialect

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: put the db manager grid on the input surface in dark mode

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: address db manager review nits on joins, boolean keys and sql quoting

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: keep the dark border color on the left pinned column edge

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: tone down the db manager tree menu icons

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: write json and jsonb values from the db manager through a text cast

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: dim unrelated diagram tables less on hover

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: match json columns as text when deleting a db manager row

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: compare postgres json columns as text in exact db manager filters

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: pick the columns the db manager grid shows

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: toggle every db manager column from one checkbox

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: word joined columns as a view in the db manager

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: keep data table roles and access visible when unavailable, with the reason

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: keep data table and instance roles visible in settings when unavailable

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: hide a db manager column from its header menu

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: format db manager columns with a unit, significant digits and color rules

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: drop the before/after hints from the db manager unit picker

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: write the euro after the amount and keep units off non-numeric values

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: color rule presets, bold and italic, layered and reorderable rules in the db manager

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: decimals, thousands separator, compact notation and alignment in db manager column formats

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: compact db manager format controls, a notation toggle group and a reset button

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: db manager format and filter nits

Keep the value's scale when decimals are auto, read boolean color-rule conditions as
booleans, let a rule's text color reach foreign-key links, close the formatter when the
columns picker opens, and filter BigQuery complex columns through TO_JSON_STRING.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 15:58:25 +02:00

166 lines
5.3 KiB
Svelte

<script lang="ts" generics="T">
import { deepEqual } from 'fast-equals'
import ConditionalPortal from '../common/drawer/ConditionalPortal.svelte'
import { untrack, type Snippet } from 'svelte'
import { twMerge } from 'tailwind-merge'
import { useReducedMotion } from '$lib/svelte5Utils.svelte'
import { watch } from 'runed'
let {
listAutoWidth = true,
strictWidth = false,
disablePortal = false,
open,
disabled = false,
class: className = '',
innerClass = '',
maxHeight = 256,
instantClose = false,
getInputRect,
portalName = 'dropdown-portal',
children
}: {
listAutoWidth?: boolean
strictWidth?: boolean
disablePortal?: boolean
open: boolean
disabled?: boolean
class?: string
innerClass?: string
maxHeight?: number
// Close with no height-collapse animation (still animates open). Used where the
// dropdown is toggled off as the user types, so the panel vanishes at once.
instantClose?: boolean
getInputRect?: () => DOMRect
/** Outside-click handlers ignore clicks in `.dropdown-portal`: a panel that holds Selects of
* its own needs another name, or their lists would not close on clicks elsewhere in it. */
portalName?: string
children?: Snippet
} = $props()
let listEl: HTMLDivElement | undefined = $state()
let dropdownPos = $state(computeDropdownPos())
let reducedMotion = useReducedMotion()
function computeDropdownPos(): {
width: number
height: number
x: number
y: number
isBelow: boolean
} {
if (!getInputRect || !listEl) return { width: 0, height: 0, x: 0, y: 0, isBelow: true }
let inputR = getInputRect()
const listR = listEl.getBoundingClientRect()
const fitsBelow = inputR.y + inputR.height + listR.height <= window.innerHeight
const spaceBelow = window.innerHeight - (inputR.y + inputR.height)
const spaceAbove = inputR.y
const isBelow = fitsBelow || spaceBelow >= spaceAbove
let [x, y] = disablePortal ? [0, 0] : [inputR.x, inputR.y]
// The list grows to its widest item, so one wider than the room right of its input
// would run off screen: slide it left instead.
if (!disablePortal) x = Math.max(0, Math.min(x, window.innerWidth - listR.width))
if (isBelow)
return { width: inputR.width, height: listR.height, x: x, y: y + inputR.height, isBelow }
else {
return { width: inputR.width, height: listR.height, x: x, y: y - listR.height, isBelow }
}
}
$effect(() => {
function updateDropdownPos() {
let nPos = computeDropdownPos()
if (!deepEqual(nPos, dropdownPos)) dropdownPos = nPos
if (open) requestAnimationFrame(updateDropdownPos)
}
if (open) untrack(() => updateDropdownPos())
})
// We do not want to render the dropdown when it is closed for performance reasons
// but we want to keep it in the DOM for a short time to allow for transitions to finish
//
// We do not use Svelte transitions because they can not animate in the opposite direction
// when the dropdown is opens above the input
// Also CSS transitions are smoother because they do not rely on JS / animation frames
let uiState = $state({
domExists: untrack(() => open),
visible: untrack(() => open),
timeout: null as number | null
})
let initial = true
watch(
() => open && !disabled,
(isOpen) => {
untrack(() => {
if (initial) {
initial = false
return
}
// Reduced motion skips all animation; instantClose skips only the closing
// one (open still animates) so the panel disappears the instant it's toggled off.
if (reducedMotion.val || (instantClose && !isOpen)) {
if (uiState.timeout) clearTimeout(uiState.timeout)
uiState = {
domExists: open && !disabled,
visible: open && !disabled,
timeout: null
}
return
}
if (uiState.timeout) clearTimeout(uiState.timeout)
uiState = {
domExists: true,
visible: !isOpen,
timeout: setTimeout(() => {
if (isOpen) {
uiState.visible = true
uiState.timeout = null
} else if (!isOpen) {
uiState.visible = false
uiState.timeout = setTimeout(() => {
uiState.domExists = false
uiState.timeout = null
}, 500) // leave time for transition to finish
}
}, 0) // We need the height to be 0 then change immediately for the transition to play
}
})
}
)
</script>
<ConditionalPortal condition={!disablePortal} name={portalName}>
{#if uiState.domExists}
<div
class={twMerge(
open ? 'dropdown-open' : 'dropdown-closed',
disablePortal ? 'absolute z-[5002]' : 'fixed z-[10000]',
'text-primary text-sm select-none',
dropdownPos.isBelow ? '' : 'flex flex-col justify-end',
uiState.visible ? '' : 'pointer-events-none',
className
)}
style="{`top: ${dropdownPos.y}px; left: ${dropdownPos.x}px;`} {listAutoWidth
? `${strictWidth ? 'width' : 'min-width'}: ${dropdownPos.width}px; height: ${dropdownPos.height}px;`
: ''}"
>
<div
class={twMerge(
'overflow-clip rounded-md drop-shadow-base',
!reducedMotion.val ? 'transition-height' : '',
dropdownPos.isBelow ? '' : 'flex flex-col justify-end'
)}
style="height: {uiState.visible ? dropdownPos.height : 0}px;"
>
<div
bind:this={listEl}
class="flex flex-col rounded-md bg-surface-input {innerClass}"
style="max-height: {maxHeight}px;"
>
{@render children?.()}
</div>
</div>
</div>
{/if}
</ConditionalPortal>