Files
windmill/frontend/src/lib/components/apps/editor/SubGridEditor.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

358 lines
9.0 KiB
Svelte

<script lang="ts">
import { stopPropagation } from 'svelte/legacy'
import { push } from '$lib/history.svelte'
import { classNames } from '$lib/utils'
import { getContext, onDestroy, untrack } from 'svelte'
import { twMerge } from 'tailwind-merge'
import { gridColumns, isFixed, toggleFixed } from '../gridUtils'
import Grid from '../svelte-grid/Grid.svelte'
import type { AppEditorContext, AppViewerContext, GridItem } from '../types'
import {
expandGriditem,
findGridItemParentGrid,
insertNewGridItem,
isContainer,
maxHeight,
selectId,
subGridIndexKey
} from './appUtils'
import Component from './component/Component.svelte'
import ComponentWrapper from './component/ComponentWrapper.svelte'
import GridViewer from './GridViewer.svelte'
import GridEditorMenu from './GridEditorMenu.svelte'
import { findGridItem } from './appUtilsCore'
interface Props {
containerHeight?: number | undefined
containerWidth?: number | undefined
class?: string
style?: string
noPadding?: boolean
noYPadding?: boolean
subGridId: string
visible?: boolean
id: string
shouldHighlight?: boolean
onFocus?: () => void
}
let {
containerHeight = undefined,
containerWidth = undefined,
class: classes = '',
style = '',
noPadding = false,
noYPadding = false,
subGridId,
visible = true,
id,
shouldHighlight = true,
onFocus
}: Props = $props()
const {
app,
connectingInput,
selectedComponent,
focusedGrid,
mode,
parentWidth,
breakpoint,
allIdsInPath,
worldStore
} = getContext<AppViewerContext>('AppViewerContext')
const editorContext = getContext<AppEditorContext>('AppEditorContext')
let isActive = $state(false)
let sber = editorContext?.componentActive?.subscribe((x) => (isActive = x))
let everVisible = $state(untrack(() => visible))
$effect.pre(() => {
visible && !everVisible && (everVisible = true)
})
onDestroy(() => {
sber?.()
})
let highlight = $derived(id === $focusedGrid?.parentComponentId && shouldHighlight)
const onpointerdown = (e) => {
onFocus?.()
}
function selectComponent(e: PointerEvent, id: string) {
if (!$connectingInput.opened) {
selectId(e, id, selectedComponent, $app)
}
}
function lock(dataItem: GridItem) {
let fComponent = findGridItem($app, dataItem.id)
if (fComponent) {
fComponent = toggleFixed(fComponent)
}
$app = $app
}
let container: HTMLElement | undefined = $state(undefined)
let maxRow = $derived(
maxHeight($app.subgrids?.[subGridId] ?? [], containerHeight ?? 0, $breakpoint)
)
export function moveComponentBetweenSubgrids(
componentId: string,
parentComponentId: string,
subGridIndex: number,
position?: { x: number; y: number }
) {
// Find the component in the source subgrid
const component = findGridItem($app, componentId)
if (!component) {
return
}
let parentGrid = findGridItemParentGrid($app, component.id)
if (parentGrid) {
$app.subgrids &&
($app.subgrids[parentGrid] = $app.subgrids[parentGrid].filter(
(item) => item.id !== component?.id
))
} else {
$app.grid = $app.grid.filter((item) => item.id !== component?.id)
}
const gridItem = component
insertNewGridItem(
$app,
(id) => ({ ...gridItem.data, id }),
{ parentComponentId: parentComponentId, subGridIndex: subGridIndex },
Object.fromEntries(gridColumns.map((column) => [column, gridItem[column]])),
component.id,
position,
undefined,
undefined,
undefined,
true
)
// Update the app state
$app = $app
if (parentGrid) {
$focusedGrid = {
parentComponentId,
subGridIndex
}
$selectedComponent = [parentComponentId]
} else {
$focusedGrid = undefined
}
}
export function moveToRoot(componentId: string, position?: { x: number; y: number }) {
// Find the component in the source subgrid
const component = findGridItem($app, componentId)
if (!component) {
return
}
let parentGrid = findGridItemParentGrid($app, component.id)
if (parentGrid) {
$app.subgrids &&
($app.subgrids[parentGrid] = $app.subgrids[parentGrid].filter(
(item) => item.id !== component?.id
))
} else {
$app.grid = $app.grid.filter((item) => item.id !== component?.id)
}
const gridItem = component
insertNewGridItem(
$app,
(id) => ({ ...gridItem.data, id }),
undefined,
Object.fromEntries(gridColumns.map((column) => [column, gridItem[column]])),
component.id,
position,
undefined,
undefined,
undefined,
true
)
// Update the app state
$app = $app
}
</script>
<!-- {visible}
{everVisible} -->
{#if everVisible || $app.eagerRendering}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="translate-x-0 translate-y-0 w-full subgrid {visible
? 'visible'
: 'invisible h-0 overflow-hidden'}"
bind:this={container}
{onpointerdown}
>
<div
class={twMerge(
$allIdsInPath.includes(id) && $mode == 'dnd' ? 'overflow-visible' : 'overflow-auto',
noYPadding ? '' : 'py-2',
classes ?? '',
noPadding ? 'px-0' : 'px-2'
)}
style="{containerHeight ? `height: ${containerHeight - 2}px;` : ''} {style ?? ''}"
>
{#if $mode !== 'preview'}
<div
class={highlight
? `!outline-dashed outline-2 min-h-full ${
isActive && !$selectedComponent?.includes(id)
? 'outline-orange-600'
: 'outline-gray-400 dark:outline-gray-600'
}`
: ''}
>
<!-- A{JSON.stringify($app.subgrids?.[subGridId]) ?? []}B -->
<Grid
allIdsInPath={$allIdsInPath}
items={$app.subgrids?.[subGridId] ?? []}
onRedraw={(grid) => {
push(editorContext?.history, $app)
if ($app.subgrids) {
$app.subgrids[subGridId] = grid
}
}}
selectedIds={$selectedComponent}
scroller={container}
parentWidth={$parentWidth - 17}
{containerWidth}
onDropped={({ id, overlapped, x, y }) => {
if (!overlapped) {
moveToRoot(id, { x, y })
} else {
const overlappedComponent = findGridItem($app, overlapped)
if (overlappedComponent && !isContainer(overlappedComponent.data.type)) {
return
}
if (!overlapped) {
return
}
if (id === overlapped) {
return
}
moveComponentBetweenSubgrids(
id,
overlapped,
subGridIndexKey(overlappedComponent?.data?.type, overlapped, $worldStore),
{ x, y }
)
}
}}
disableMove={!!$connectingInput.opened}
>
{#snippet children({ dataItem, overlapped, componentDraggedId })}
<ComponentWrapper
id={dataItem.id}
type={dataItem.data.type}
class={classNames(
'h-full w-full center-center',
$selectedComponent?.includes(dataItem.id) ? 'active-grid-item' : '',
'top-0'
)}
>
<GridEditorMenu id={dataItem.id}>
<Component
{overlapped}
fullHeight={dataItem?.[$breakpoint === 'sm' ? 3 : 12]?.fullHeight}
render={visible}
component={dataItem.data}
selected={Boolean($selectedComponent?.includes(dataItem.id))}
locked={isFixed(dataItem)}
on:lock={() => lock(dataItem)}
on:expand={() => {
const parentGridItem = findGridItem($app, id)
if (!parentGridItem) {
return
}
$selectedComponent = [dataItem.id]
push(editorContext?.history, $app)
expandGriditem(
$app.subgrids?.[subGridId] ?? [],
dataItem.id,
$breakpoint,
parentGridItem
)
$app = $app
}}
on:fillHeight={() => {
const gridItem = findGridItem($app, dataItem.id)
const b = $breakpoint === 'sm' ? 3 : 12
if (gridItem?.[b]) {
gridItem[b].fullHeight = !gridItem[b].fullHeight
}
$app = $app
}}
{componentDraggedId}
/>
</GridEditorMenu>
</ComponentWrapper>
{/snippet}
</Grid>
</div>
{:else}
<GridViewer
allIdsInPath={$allIdsInPath}
items={$app.subgrids?.[subGridId] ?? []}
breakpoint={$breakpoint}
parentWidth={$parentWidth - 17}
{containerWidth}
{maxRow}
>
{#snippet children({ dataItem })}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
onpointerdown={stopPropagation((e) =>
selectComponent(e as PointerEvent, dataItem.id)
)}
class={classNames('h-full w-full center-center', 'top-0')}
>
<Component
fullHeight={dataItem?.[$breakpoint === 'sm' ? 3 : 12]?.fullHeight}
render={visible}
component={dataItem.data}
selected={Boolean($selectedComponent?.includes(dataItem.id))}
locked={isFixed(dataItem)}
/>
</div>
{/snippet}
</GridViewer>
{/if}
</div>
</div>
{:else if $app.lazyInitRequire == undefined}
{#each $app?.subgrids?.[subGridId] ?? [] as item}
<Component selected={false} fullHeight={false} render={false} component={item.data} />
{/each}
{/if}