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

310 lines
8.2 KiB
Svelte

<script lang="ts">
import { getContext, untrack } from 'svelte'
import type { AppEditorContext, AppViewerContext } from '../types'
import { gridColumns, isFixed, toggleFixed } from '../gridUtils'
import { twMerge } from 'tailwind-merge'
import HiddenComponent from '../components/helpers/HiddenComponent.svelte'
import Component from './component/Component.svelte'
import { push } from '$lib/history.svelte'
import {
dfs,
expandGriditem,
findGridItemParentGrid,
insertNewGridItem,
isContainer,
subGridIndexKey
} from './appUtils'
import Grid from '../svelte-grid/Grid.svelte'
import { deepEqual } from 'fast-equals'
import ComponentWrapper from './component/ComponentWrapper.svelte'
import { classNames } from '$lib/utils'
import GridEditorMenu from './GridEditorMenu.svelte'
import RecomputeAllComponents from './RecomputeAllComponents.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import { Loader2 } from 'lucide-svelte'
import Popover from '$lib/components/Popover.svelte'
import type { Policy } from '$lib/gen'
import { BG_PREFIX, findGridItem } from './appUtilsCore'
interface Props {
policy: Policy
}
let { policy }: Props = $props()
const {
selectedComponent,
app,
connectingInput,
summary,
focusedGrid,
parentWidth,
breakpoint,
allIdsInPath,
bgRuns,
worldStore
} = getContext<AppViewerContext>('AppViewerContext')
const { history, componentActive } = getContext<AppEditorContext>('AppEditorContext')
let previousSelectedIds: string[] | undefined = $state(undefined)
$effect(() => {
if (!deepEqual(previousSelectedIds, $selectedComponent)) {
untrack(() => {
previousSelectedIds = $selectedComponent
$allIdsInPath = ($selectedComponent ?? [])
.flatMap((id) => dfs($app.grid, id, $app.subgrids ?? {}))
.filter((x) => x != undefined) as string[]
})
}
})
function handleLock(id: string) {
const gridItem = findGridItem($app, id)
if (gridItem) {
toggleFixed(gridItem)
}
$app = $app
}
function handleFillHeight(id: string) {
const gridItem = findGridItem($app, id)
const b = $breakpoint === 'sm' ? 3 : 12
if (gridItem?.[b]) {
gridItem[b].fullHeight = !gridItem[b].fullHeight
}
$app = $app
}
export function moveComponentBetweenSubgrids(
componentId: string,
subgrid: { parentComponentId: string; subGridIndex: number } | undefined,
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 }),
subgrid,
Object.fromEntries(gridColumns.map((column) => [column, gridItem[column]])),
component.id,
position,
undefined,
undefined,
undefined,
true
)
// Update the app state
$app = $app
if (subgrid) {
$selectedComponent = [subgrid.parentComponentId]
$focusedGrid = subgrid
} else {
$selectedComponent = [componentId]
$focusedGrid = undefined
}
}
</script>
<div class="w-full z-[1000] overflow-visible h-full">
<div class={$app.hideLegacyTopBar ? 'hidden' : ''}>
<div
class="w-full sticky top-0 flex justify-between border-b {$componentActive
? 'invisible'
: 'z-50'} {$connectingInput?.opened ? '' : 'bg-surface'} px-4 py-1 items-center gap-4"
>
<h3 class="truncate">{$summary}</h3>
<div class="flex gap-2 items-center">
<div>
{#if !$connectingInput.opened}
<RecomputeAllComponents />
{/if}
</div>
{#if $bgRuns.length > 0}
<Popover notClickable>
<span class="!text-2xs text-primary inline-flex gap-1 items-center"
><Loader2 size={10} class="animate-spin" /> {$bgRuns.length}
</span>
{#snippet text()}
<span
><div class="flex flex-col">
{#each $bgRuns as bgRun}
<div class="flex gap-2 items-center">
<div class="text-2xs text-primary">{bgRun}</div>
</div>
{/each}
</div></span
>
{/snippet}
</Popover>
{:else}
<span class="w-9"></span>
{/if}
</div>
<div class="flex text-2xs gap-8 items-center">
<div class="py-2 pr-2 text-secondary flex gap-1 items-center">
Hide bar on view
<Toggle
size="xs"
bind:checked={
() => $app.norefreshbar ?? false,
(v) => ($app.norefreshbar !== undefined || v) && ($app.norefreshbar = v)
}
/>
</div>
<div>
{policy.on_behalf_of ? `Author ${policy.on_behalf_of_email}` : ''}
<Tooltip>
The scripts will be run on behalf of the author and a tight policy ensure security about
the possible inputs of the runnables.
</Tooltip>
</div>
</div>
</div>
</div>
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
style={$app.css?.['app']?.['grid']?.style}
class={twMerge(
'p-2 overflow-visible z-50',
$app.css?.['app']?.['grid']?.class ?? '',
'wm-app-grid !static h-full w-full'
)}
onpointerdown={() => {
$selectedComponent = undefined
$focusedGrid = undefined
}}
bind:clientWidth={$parentWidth}
>
<div
class="subgrid overflow-visible z-100 outline-dashed outline-2 outline-offset-4 outline-gray-300 dark:outline-gray-500"
style={`transform: scale(1);`}
>
<Grid
allIdsInPath={$allIdsInPath}
selectedIds={$selectedComponent}
items={$app.grid}
onRedraw={(grid) => {
push(history, $app)
$app.grid = grid
}}
root
onDropped={({ id, overlapped, x, y }) => {
const overlappedComponent = overlapped ? findGridItem($app, overlapped) : undefined
if (overlappedComponent && !isContainer(overlappedComponent.data.type)) {
return
}
if (id === overlapped) {
return
}
moveComponentBetweenSubgrids(
id,
overlapped
? {
parentComponentId: overlapped,
subGridIndex: subGridIndexKey(
overlappedComponent?.data?.type,
overlapped,
$worldStore
)
}
: undefined,
{ 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 outline outline-surface-secondary',
Boolean($selectedComponent?.includes(dataItem.id)) ? 'active-grid-item' : ''
)}
>
<GridEditorMenu
id={dataItem.id}
on:expand={() => {
push(history, $app)
$selectedComponent = [dataItem.id]
expandGriditem($app.grid, dataItem.id, $breakpoint)
$app = $app
}}
on:lock={() => {
handleLock(dataItem.id)
}}
on:fillHeight={() => {
handleFillHeight(dataItem.id)
}}
locked={isFixed(dataItem)}
fullHeight={dataItem?.[$breakpoint === 'sm' ? 3 : 12]?.fullHeight}
>
<Component
render={true}
component={dataItem.data}
selected={Boolean($selectedComponent?.includes(dataItem.id))}
locked={isFixed(dataItem)}
fullHeight={dataItem?.[$breakpoint === 'sm' ? 3 : 12]?.fullHeight}
on:lock={() => {
handleLock(dataItem.id)
}}
on:fillHeight={() => {
handleFillHeight(dataItem.id)
}}
on:expand={() => {
push(history, $app)
$selectedComponent = [dataItem.id]
expandGriditem($app.grid, dataItem.id, $breakpoint)
$app = $app
}}
{overlapped}
{componentDraggedId}
/>
</GridEditorMenu>
</ComponentWrapper>
{/snippet}
</Grid>
</div>
</div>
</div>
{#if $app.hiddenInlineScripts}
{#each $app.hiddenInlineScripts as runnable, index}
{#if runnable}
<HiddenComponent id={BG_PREFIX + index} {runnable} />
{/if}
{/each}
{/if}
<style global>
.svlt-grid-active {
opacity: 1 !important;
}
</style>