Files
windmill/frontend/src/lib/components/wizards/DBExplorerWizard.svelte
T
Diego ImbertandClaude Opus 4.6 5d79f33590 Final Svelte 5 migration (#8211)
* Remove $$props.field usage

* Rename slots to ensure no hyphen

* _props

* _trigger

* OnSelectedIteration type correct capitalization

* rename _content

* Remove afterUpdate

* Migrate everything to svelte 5

* array bind

* Fix popover

* type never

* nit fixes

* Fixed many trivial errors

* onClick

* Fix errors

* use let:

* nit typing

* fix: wrap state_referenced_locally vars with untrack()

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add untrack import

* Fix all syntax errors due to untrack migration

* Fix undefined errors

* Fix more undefined errors

* untrack(() => initialOpen)

* svelte-ignore

* Fix state_descriptors_fixed error in Chart.svelte

Use $state.snapshot() to pass plain copies of data/options to Chart.js
instead of $state proxies. Chart.js's listenArrayEvents tries to define
property descriptors on data arrays, which Svelte 5 proxies reject.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* nit typing

* Merge issue

* Fix "path is not set" error in resource picker / editor

* Fix InputTransformForm error when rerunning some flows

* fix npm run check

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-05 18:11:40 +01:00

401 lines
11 KiB
Svelte

<script lang="ts">
import { Badge, type AlertType } from '../common'
import Popover from '$lib/components/meltComponents/Popover.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import SimpleEditor from '../SimpleEditor.svelte'
import Label from '../Label.svelte'
import Section from '../Section.svelte'
import Tooltip from '../Tooltip.svelte'
import Button from '../common/button/Button.svelte'
import { twMerge } from 'tailwind-merge'
import { ColumnIdentity, type ColumnDef } from '../apps/components/display/dbtable/utils'
import { offset, flip, shift } from 'svelte-floating-ui/dom'
import Alert from '../common/alert/Alert.svelte'
interface Props {
value: ColumnDef | undefined
trigger?: import('svelte').Snippet
}
let { value = $bindable(), trigger: trigger_render }: Props = $props()
const presets = [
{
label: 'None',
value: null
},
{
label: 'Currency CHF',
value: 'value + " CHF"'
},
{
label: 'Currency USD',
value: '"$ " + value'
},
{
label: 'Date',
value: 'new Date(value).toLocaleDateString()'
},
{
label: 'Percentage',
value: 'value + " %"'
},
{
label: 'Currency GBP',
value: 'value + " £"'
},
{
label: 'Currency EUR',
value: 'value + " €"'
},
{
label: 'Currency JPY',
value: 'value + " ¥"'
},
{
label: 'Decimal places (2)',
value: 'parseFloat(value).toFixed(2)'
},
{
label: 'Uppercase',
value: 'value.toUpperCase()'
},
{
label: 'Lowercase',
value: 'value.toLowerCase()'
},
{
label: 'Boolean (True/False)',
value: 'value ? "True" : "False"'
},
{
label: 'Object',
value: 'JSON.stringify(value, null, 2)'
}
]
let renderCount = $state(0)
function computeWarning(columnMetadata, value) {
if (columnMetadata?.isnullable === 'NO' && !columnMetadata?.defaultvalue) {
if ([ColumnIdentity.ByDefault].includes(columnMetadata?.isidentity)) {
return {
type: 'info' as AlertType,
title: 'Value will be generated',
message:
'The column is an identity column. The value will be generated by the database unless a default is provided.'
}
}
if (value?.hideInsert && columnMetadata?.isidentity !== ColumnIdentity.Always) {
return {
type: 'warning' as AlertType,
title: 'No default value',
message:
"The column is not nullable and doesn't have a default value. A default value is required."
}
}
}
if (columnMetadata?.defaultvalue !== null) {
return {
type: 'info' as AlertType,
title: 'Default value',
message: `${
value?.hideInsert ? '' : 'You may want to hide this field from insert. '
}The column has a default value defined in the database. The default value is: ${
value?.defaultvalue
}`
}
}
if (columnMetadata?.isnullable === 'YES') {
return {
type: 'info' as AlertType,
title: 'Default value',
message: `${
value?.hideInsert ? '' : 'You may want to hide this field from insert. '
}The column can be null. If no value is provided, the default value will be null.`
}
}
return null
}
let warning = $derived(computeWarning(value, value))
</script>
<Popover
floatingConfig={{
strategy: 'fixed',
placement: 'left-start',
middleware: [offset(8), flip(), shift()]
}}
contentClasses="max-h-[70vh] overflow-y-auto p-4 flex flex-col gap-4 w-96"
closeOnOtherPopoverOpen
>
{#snippet trigger()}
{@render trigger_render?.()}
{/snippet}
{#snippet content()}
{#if value}
<Section label="Column settings">
{#snippet header()}
<Badge color="blue">
{value.field}
</Badge>
{/snippet}
<Label label="Skip for select and update">
{#snippet header()}
<Tooltip>
By default, all columns are included in the select and update queries. If you want to
exclude a column from the select and update queries, you can set this property to
true.
</Tooltip>
{/snippet}
{#snippet action()}
<Toggle
on:pointerdown={(e) => {
e?.stopPropagation()
}}
bind:checked={value.ignored}
size="xs"
disabled={value?.isprimarykey}
/>
{/snippet}
{#if value?.isprimarykey}
<Alert type="warning" size="xs" title="Primary key" class="my-1">
You cannot skip a primary key.
</Alert>
{/if}
</Label>
<Label label="Hide from insert">
{#snippet header()}
<Tooltip>
By default, all columns are used to generate the submit form. If you want to exclude a
column from the submit form, you can set this property to true. If the column is not
nullable or doesn't have a default value, a default value will be required.
</Tooltip>
{/snippet}
{#snippet action()}
<Toggle
disabled={value?.isidentity === ColumnIdentity.Always}
on:pointerdown={(e) => {
e?.stopPropagation()
}}
bind:checked={value.hideInsert}
size="xs"
/>
{/snippet}
</Label>
{#if value?.isidentity === ColumnIdentity.Always}
<Alert type="warning" size="xs" title="Identity column" class="my-1">
This column is an ALWAYS identity column and so can't be provided by the user.
</Alert>
{/if}
{#if warning}
<Alert type={warning.type} size="xs" title={warning.title} class="my-2">
{warning.message}
</Alert>
{/if}
{#if value?.defaultvalue !== null && value?.hideInsert}
<Toggle
bind:checked={value.overrideDefaultValue}
size="xs"
options={{
right: `Override default value: ${value?.defaultvalue}`
}}
on:change={() => {
if (!value || !value.overrideDefaultValue) {
if (value) {
value.defaultValueNull = false
value.defaultUserValue = undefined
}
}
}}
/>
{/if}
<Label label="Default input">
{#snippet header()}
<Tooltip>
By default, all columns are used to generate the submit form. If you want to exclude a
column from the submit form, you can set this property to true. If the column is not
nullable or doesn't have a default value, a default value will be required.
</Tooltip>
{/snippet}
{#if value?.datatype}
{@const type = value?.datatype}
<div class="flex flex-row items-center gap-2">
<Badge color="dark-gray">
Type:
{type}
</Badge>
{#if value?.isnullable == 'YES' && value.hideInsert}
<Toggle
bind:checked={value.defaultValueNull}
size="xs"
options={{
right: 'Set to null'
}}
disabled={value.hideInsert &&
value?.defaultvalue !== null &&
!value?.overrideDefaultValue}
on:change={() => {
if (value?.defaultValueNull && value) {
value.defaultUserValue = null
}
}}
/>
{/if}
</div>
<input
type="text"
placeholder="Default value"
class="mt-2"
bind:value={value.defaultUserValue}
disabled={value.defaultValueNull ||
(value.hideInsert && value?.defaultvalue !== null && !value?.overrideDefaultValue)}
/>
{/if}
</Label>
</Section>
<Section label="AG Grid configuration">
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class={twMerge('flex flex-col gap-4', value.ignored ? 'opacity-50 cursor-none ' : '')}
onpointerdown={(e) => {
if (value?.ignored) {
e?.stopPropagation()
}
}}
>
<Label label="Header name">
<input type="text" placeholder="Header name" bind:value={value.headerName} />
</Label>
<Label label="Editable value">
<Toggle
on:pointerdown={(e) => {
e?.stopPropagation()
}}
options={{ right: 'Editable' }}
bind:checked={value.editable}
size="xs"
/>
</Label>
<Label label="Min width (px)">
<input type="number" placeholder="width" bind:value={value.minWidth} />
</Label>
<Label label="Flex">
{#snippet header()}
<Tooltip
documentationLink="https://www.ag-grid.com/javascript-data-grid/column-sizing/#column-flex"
>
It's often required that one or more columns fill the entire available space in the
grid. For this scenario, it is possible to use the flex config. Some columns could
be set with a regular width config, while other columns would have a flex config.
Flex sizing works by dividing the remaining space in the grid among all flex columns
in proportion to their flex value. For example, suppose the grid has a total width
of 450px and it has three columns: the first with width: 150; the second with flex:
1; and third with flex: 2. The first column will be 150px wide, leaving 300px
remaining. The column with flex: 2 has twice the size with flex: 1. So final sizes
will be: 150px, 100px, 200px.
</Tooltip>
{/snippet}
<input type="range" step="1" bind:value={value.flex} min={1} max={12} />
<div class="text-xs">{value.flex}</div>
</Label>
<Label label="Hide">
<Toggle
on:pointerdown={(e) => {
e?.stopPropagation()
}}
options={{ right: 'Hide' }}
bind:checked={value.hide}
size="xs"
/>
</Label>
<Label label="Value formatter">
{#snippet header()}
<Tooltip
documentationLink="https://www.ag-grid.com/javascript-data-grid/value-formatters/"
>
Value formatters allow you to format values for display. This is useful when data is
one type (e.g. numeric) but needs to be converted for human reading (e.g. putting in
currency symbols and number formatting).
</Tooltip>
{/snippet}
{#snippet action()}
<Button
size="xs"
variant="default"
on:click={() => {
// @ts-ignore
value.valueFormatter = null
renderCount++
}}
>
Clear
</Button>
{/snippet}
</Label>
<div>
{#key renderCount}
<div class="flex flex-col gap-4">
<div class="relative">
{#if !presets.find((preset) => preset.value === value?.valueFormatter)}
<div
class="z-50 absolute bg-opacity-50 bg-surface top-0 left-0 bottom-0 right-0"
></div>
{/if}
<div class="text-xs font-semibold">Presets</div>
<select
bind:value={value.valueFormatter}
onchange={() => {
renderCount++
}}
placeholder="Code"
>
{#each presets as preset}
<option value={preset.value}>{preset.label}</option>
{/each}
</select>
</div>
<SimpleEditor
extraLib={'declare const value: any'}
autoHeight
lang="javascript"
bind:code={value.valueFormatter}
/>
<div class="text-xs text-secondary -mt-4">Use `value` in the formatter</div>
</div>
{/key}
</div>
<Label label="Sort">
<select bind:value={value.sort}>
<option value={null}>None</option>
<option value="asc">Ascending</option>
<option value="desc">Descending</option>
</select>
</Label>
</div>
</Section>
{/if}
{/snippet}
</Popover>