mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-07 00:01:49 +00:00
feat(frontend): add ability to move nodes
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation'
|
||||
import { FlowService, ScheduleService, type Flow } from '$lib/gen'
|
||||
import { FlowService, ScheduleService, type Flow, type FlowModule } from '$lib/gen'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { encodeState, formatCron, loadHubScripts, sendUserToast } from '$lib/utils'
|
||||
import { faCalendarAlt, faEye, faPen, faSave } from '@fortawesome/free-solid-svg-icons'
|
||||
@@ -144,6 +144,7 @@
|
||||
const scheduleStore = writable<Schedule>({ args: {}, cron: '', enabled: false })
|
||||
const previewArgsStore = writable<Record<string, any>>(initialArgs)
|
||||
const scriptEditorDrawer = writable<ScriptEditorDrawer | undefined>(undefined)
|
||||
const moving = writable<{ module: FlowModule; modules: FlowModule[] } | undefined>(undefined)
|
||||
|
||||
function select(selectedId: string) {
|
||||
selectedIdStore.set(selectedId)
|
||||
@@ -153,7 +154,8 @@
|
||||
selectedId: selectedIdStore,
|
||||
schedule: scheduleStore,
|
||||
previewArgs: previewArgsStore,
|
||||
scriptEditorDrawer
|
||||
scriptEditorDrawer,
|
||||
moving
|
||||
})
|
||||
|
||||
async function loadSchedule() {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
import { Bed, Move, PhoneIncoming, Repeat, Square, X } from 'lucide-svelte'
|
||||
import type { FlowModule } from '$lib/gen'
|
||||
|
||||
export let selected: boolean = false
|
||||
export let deletable: boolean = false
|
||||
@@ -103,14 +104,14 @@
|
||||
<X size={12} strokeWidth={2} />
|
||||
</button>
|
||||
|
||||
<!-- <button
|
||||
<button
|
||||
class="absolute -top-2 right-10 rounded-full h-4 w-4 trash center-center
|
||||
border-[1.5px] border-gray-700 bg-white duration-150 hover:bg-blue-400 hover:text-white
|
||||
hover:border-blue-700 {selected ? '' : '!hidden'}"
|
||||
on:click|preventDefault|stopPropagation={(event) => dispatch('move', { module })}
|
||||
on:click|preventDefault|stopPropagation={(event) => dispatch('move')}
|
||||
>
|
||||
<Move size={12} strokeWidth={2} />
|
||||
</button> -->
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
let idToRemove: string | undefined = undefined
|
||||
|
||||
const { selectedId } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
const { selectedId, moving } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
async function insertNewModuleAtIndex(
|
||||
modules: FlowModule[],
|
||||
@@ -81,41 +81,6 @@
|
||||
})
|
||||
}
|
||||
|
||||
// async function insertModule(
|
||||
// modules: FlowModule[],
|
||||
// id: string,
|
||||
// kind: string,
|
||||
// where: 'before' | 'after'
|
||||
// ): Promise<FlowModule[]> {
|
||||
// console.log(modules, id, kind, where)
|
||||
// const index = modules.findIndex((mod) => mod.id == id)
|
||||
// if (index != -1) {
|
||||
// console.log('INSERTING', modules, index)
|
||||
// return await insertNewModuleAtIndex(modules, index + (where == 'after' ? 1 : 0), kind as any)
|
||||
// }
|
||||
// return await Promise.all(
|
||||
// modules.map(async (mod) => {
|
||||
// if (mod.value.type == 'forloopflow') {
|
||||
// mod.value.modules = await insertModule(mod.value.modules, id, kind, where)
|
||||
// } else if (mod.value.type == 'branchall') {
|
||||
// mod.value.branches = await Promise.all(
|
||||
// mod.value.branches.map(async (branch) => {
|
||||
// branch.modules = await insertModule(branch.modules, id, kind, where)
|
||||
// return branch
|
||||
// })
|
||||
// )
|
||||
// } else if (mod.value.type == 'branchone') {
|
||||
// mod.value.branches = await Promismod.value.branches.map((branch) => {
|
||||
// branch.modules = await insertModule(branch.modules, id, kind, where)
|
||||
// return branch
|
||||
// })
|
||||
// mod.value.default = await insertModule(mod.value.default, id, kind, where)
|
||||
// }
|
||||
// return mod
|
||||
// })
|
||||
// )
|
||||
// }
|
||||
|
||||
$: confirmationModalOpen = idToRemove !== undefined
|
||||
|
||||
$: sidebarMode == 'graph' ? (sidebarSize = 40) : (sidebarSize = 20)
|
||||
@@ -171,6 +136,7 @@
|
||||
<FlowGraph
|
||||
insertable
|
||||
{minHeight}
|
||||
moving={$moving?.module.id}
|
||||
rebuildOnChange={$flowStore}
|
||||
maxHeight={minHeight}
|
||||
modules={$flowStore.value?.modules}
|
||||
@@ -187,7 +153,14 @@
|
||||
}}
|
||||
on:insert={async ({ detail }) => {
|
||||
if (detail.modules) {
|
||||
await insertNewModuleAtIndex(detail.modules, detail.index ?? 0, detail.detail)
|
||||
if ($moving) {
|
||||
let indexToRemove = $moving.modules.findIndex((m) => $moving?.module?.id == m.id)
|
||||
$moving.modules.splice(indexToRemove, 1)
|
||||
detail.modules.splice(detail.index, 0, $moving.module)
|
||||
$moving = undefined
|
||||
} else {
|
||||
await insertNewModuleAtIndex(detail.modules, detail.index ?? 0, detail.detail)
|
||||
}
|
||||
$flowStore = $flowStore
|
||||
$selectedId = detail.modules[detail.index ?? 0].id
|
||||
}
|
||||
@@ -205,6 +178,16 @@
|
||||
$selectedId = detail.module.id
|
||||
}
|
||||
}}
|
||||
on:move={async ({ detail }) => {
|
||||
if (!$moving || $moving.module.id !== detail.module.id) {
|
||||
if (detail.module && detail.modules) {
|
||||
console.log('MOVE+')
|
||||
$moving = { module: detail.module, modules: detail.modules }
|
||||
}
|
||||
} else {
|
||||
$moving = undefined
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
import Icon from 'svelte-awesome'
|
||||
import IconedResourceType from '$lib/components/IconedResourceType.svelte'
|
||||
import LanguageIcon from '$lib/components/common/languageIcons/LanguageIcon.svelte'
|
||||
import { Building, GitBranchPlus, Repeat, Square } from 'lucide-svelte'
|
||||
import { Building, ClipboardCopy, GitBranchPlus, Move, Repeat, Square } from 'lucide-svelte'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import { Button } from '$lib/components/common'
|
||||
|
||||
export let mod: FlowModule
|
||||
export let trigger: boolean
|
||||
@@ -19,6 +20,7 @@
|
||||
export let branchable: boolean = false
|
||||
export let bgColor: string = ''
|
||||
export let modules: FlowModule[]
|
||||
export let moving: string | undefined = undefined
|
||||
|
||||
$: idx = modules.findIndex((m) => m.id === mod.id)
|
||||
|
||||
@@ -28,10 +30,11 @@
|
||||
insert: {
|
||||
modules: FlowModule[]
|
||||
index: number
|
||||
detail: 'script' | 'forloop' | 'branchone' | 'branchall'
|
||||
detail: 'script' | 'forloop' | 'branchone' | 'branchall' | 'move'
|
||||
}
|
||||
select: string
|
||||
newBranch: { module: FlowModule }
|
||||
move: { module: FlowModule }
|
||||
}>()
|
||||
|
||||
$: itemProps = {
|
||||
@@ -54,107 +57,149 @@
|
||||
<div
|
||||
class="{openMenu ? 'z-10' : ''} w-7 absolute -top-9 left-[50%] right-[50%] -translate-x-1/2"
|
||||
>
|
||||
<InsertModuleButton
|
||||
bind:open={openMenu}
|
||||
{trigger}
|
||||
on:new={(e) => {
|
||||
dispatch('insert', { modules, index: idx, detail: e.detail })
|
||||
}}
|
||||
/>
|
||||
{#if moving}
|
||||
<button
|
||||
title="Add branch"
|
||||
on:click={() => {
|
||||
dispatch('insert', { modules, index: idx, detail: 'move' })
|
||||
}}
|
||||
type="button"
|
||||
class=" text-gray-900 bg-white border mx-0.5 border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 font-medium rounded-full text-sm w-6 h-6 flex items-center justify-center"
|
||||
>
|
||||
<ClipboardCopy size={12} />
|
||||
</button>
|
||||
{:else}
|
||||
<InsertModuleButton
|
||||
bind:open={openMenu}
|
||||
{trigger}
|
||||
on:new={(e) => {
|
||||
dispatch('insert', { modules, index: idx, detail: e.detail })
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if mod.value.type === 'forloopflow'}
|
||||
<FlowModuleSchemaItem
|
||||
deletable={insertable}
|
||||
label={mod.summary || 'For loop'}
|
||||
id={mod.id}
|
||||
on:delete={onDelete}
|
||||
on:click={() => dispatch('select', mod.id)}
|
||||
{...itemProps}
|
||||
{bgColor}
|
||||
>
|
||||
<div slot="icon">
|
||||
<Repeat size={16} />
|
||||
<div class="relative">
|
||||
{#if moving == mod.id}
|
||||
<div class="absolute z-10 right-20 top-0.5 center-center">
|
||||
<Button color="dark" on:click={() => dispatch('move')} size="xs" variant="border"
|
||||
>Cancel move</Button
|
||||
>
|
||||
</div>
|
||||
</FlowModuleSchemaItem>
|
||||
{:else if mod.value.type === 'branchone'}
|
||||
<FlowModuleSchemaItem
|
||||
deletable={insertable}
|
||||
on:delete={onDelete}
|
||||
on:click={() => dispatch('select', mod.id)}
|
||||
{...itemProps}
|
||||
id={mod.id}
|
||||
label={mod.summary || 'Run one branch'}
|
||||
{bgColor}
|
||||
>
|
||||
<div slot="icon">
|
||||
<Icon data={faCodeBranch} scale={1} />
|
||||
</div>
|
||||
</FlowModuleSchemaItem>
|
||||
{:else if mod.value.type === 'branchall'}
|
||||
<FlowModuleSchemaItem
|
||||
deletable={insertable}
|
||||
on:delete={onDelete}
|
||||
on:click={() => dispatch('select', mod.id)}
|
||||
id={mod.id}
|
||||
{...itemProps}
|
||||
label={mod.summary || 'Run all branches'}
|
||||
{bgColor}
|
||||
>
|
||||
<div slot="icon">
|
||||
<Icon data={faCodeBranch} scale={1} />
|
||||
</div>
|
||||
</FlowModuleSchemaItem>
|
||||
{:else}
|
||||
<FlowModuleSchemaItem
|
||||
on:click={() => dispatch('select', mod.id)}
|
||||
on:delete={onDelete}
|
||||
deletable={insertable}
|
||||
id={mod.id}
|
||||
{...itemProps}
|
||||
modType={mod.value.type}
|
||||
{bgColor}
|
||||
label={mod.summary ||
|
||||
(`path` in mod.value ? mod.value.path : undefined) ||
|
||||
(mod.value.type === 'rawscript' ? `Inline ${mod.value.language}` : 'To be defined')}
|
||||
>
|
||||
<div slot="icon">
|
||||
{#if mod.value.type === 'rawscript'}
|
||||
<LanguageIcon lang={mod.value.language} width={16} height={16} />
|
||||
{:else if mod.summary == 'Terminate flow'}
|
||||
<Square size={16} />
|
||||
{:else if mod.value.type === 'identity'}
|
||||
<Icon data={faLongArrowDown} scale={1.1} />
|
||||
{:else if mod.value.type === 'flow'}
|
||||
<Icon data={faBarsStaggered} scale={1.0} />
|
||||
{:else if mod.value.type === 'script'}
|
||||
{#if mod.value.path.startsWith('hub/')}
|
||||
<div>
|
||||
<IconedResourceType
|
||||
width="20px"
|
||||
height="20px"
|
||||
name={mod.value.path.split('/')[2]}
|
||||
silent={true}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<Building size={14} />
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</FlowModuleSchemaItem>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div class={moving == mod.id ? 'opacity-50' : ''}>
|
||||
{#if mod.value.type === 'forloopflow'}
|
||||
<FlowModuleSchemaItem
|
||||
deletable={insertable}
|
||||
label={mod.summary || 'For loop'}
|
||||
id={mod.id}
|
||||
on:move={() => dispatch('move')}
|
||||
on:delete={onDelete}
|
||||
on:click={() => dispatch('select', mod.id)}
|
||||
{...itemProps}
|
||||
{bgColor}
|
||||
>
|
||||
<div slot="icon">
|
||||
<Repeat size={16} />
|
||||
</div>
|
||||
</FlowModuleSchemaItem>
|
||||
{:else if mod.value.type === 'branchone'}
|
||||
<FlowModuleSchemaItem
|
||||
deletable={insertable}
|
||||
on:delete={onDelete}
|
||||
on:move={() => dispatch('move')}
|
||||
on:click={() => dispatch('select', mod.id)}
|
||||
{...itemProps}
|
||||
id={mod.id}
|
||||
label={mod.summary || 'Run one branch'}
|
||||
{bgColor}
|
||||
>
|
||||
<div slot="icon">
|
||||
<Icon data={faCodeBranch} scale={1} />
|
||||
</div>
|
||||
</FlowModuleSchemaItem>
|
||||
{:else if mod.value.type === 'branchall'}
|
||||
<FlowModuleSchemaItem
|
||||
deletable={insertable}
|
||||
on:delete={onDelete}
|
||||
on:move={() => dispatch('move')}
|
||||
on:click={() => dispatch('select', mod.id)}
|
||||
id={mod.id}
|
||||
{...itemProps}
|
||||
label={mod.summary || 'Run all branches'}
|
||||
{bgColor}
|
||||
>
|
||||
<div slot="icon">
|
||||
<Icon data={faCodeBranch} scale={1} />
|
||||
</div>
|
||||
</FlowModuleSchemaItem>
|
||||
{:else}
|
||||
<FlowModuleSchemaItem
|
||||
on:click={() => dispatch('select', mod.id)}
|
||||
on:delete={onDelete}
|
||||
on:move={() => dispatch('move')}
|
||||
deletable={insertable}
|
||||
id={mod.id}
|
||||
{...itemProps}
|
||||
modType={mod.value.type}
|
||||
{bgColor}
|
||||
label={mod.summary ||
|
||||
(`path` in mod.value ? mod.value.path : undefined) ||
|
||||
(mod.value.type === 'rawscript' ? `Inline ${mod.value.language}` : 'To be defined')}
|
||||
>
|
||||
<div slot="icon">
|
||||
{#if mod.value.type === 'rawscript'}
|
||||
<LanguageIcon lang={mod.value.language} width={16} height={16} />
|
||||
{:else if mod.summary == 'Terminate flow'}
|
||||
<Square size={16} />
|
||||
{:else if mod.value.type === 'identity'}
|
||||
<Icon data={faLongArrowDown} scale={1.1} />
|
||||
{:else if mod.value.type === 'flow'}
|
||||
<Icon data={faBarsStaggered} scale={1.0} />
|
||||
{:else if mod.value.type === 'script'}
|
||||
{#if mod.value.path.startsWith('hub/')}
|
||||
<div>
|
||||
<IconedResourceType
|
||||
width="20px"
|
||||
height="20px"
|
||||
name={mod.value.path.split('/')[2]}
|
||||
silent={true}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<Building size={14} />
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</FlowModuleSchemaItem>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if insertable && insertableEnd}
|
||||
<div
|
||||
class="{openMenu2 ? 'z-10' : ''} w-7 absolute top-11 left-[50%] right-[50%] -translate-x-1/2"
|
||||
>
|
||||
<InsertModuleButton
|
||||
bind:open={openMenu2}
|
||||
{trigger}
|
||||
on:new={(e) => {
|
||||
dispatch('insert', { modules, index: idx + 1, detail: e.detail })
|
||||
}}
|
||||
/>
|
||||
{#if moving}
|
||||
<button
|
||||
title="Add branch"
|
||||
on:click={() => {
|
||||
dispatch('insert', { modules, index: idx + 1, detail: 'move' })
|
||||
}}
|
||||
type="button"
|
||||
class=" text-gray-900 bg-white border mx-0.5 border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 font-medium rounded-full text-sm w-6 h-6 flex items-center justify-center"
|
||||
>
|
||||
<ClipboardCopy size={12} />
|
||||
</button>
|
||||
{:else}
|
||||
<InsertModuleButton
|
||||
bind:open={openMenu2}
|
||||
{trigger}
|
||||
on:new={(e) => {
|
||||
dispatch('insert', { modules, index: idx + 1, detail: e.detail })
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import type { FlowModule } from '$lib/gen'
|
||||
import { classNames } from '$lib/utils'
|
||||
import { faBolt } from '@fortawesome/free-solid-svg-icons'
|
||||
import { X } from 'lucide-svelte'
|
||||
import { ClipboardCopy, X } from 'lucide-svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { Icon } from 'svelte-awesome'
|
||||
import InsertModuleButton from './InsertModuleButton.svelte'
|
||||
@@ -18,10 +18,11 @@
|
||||
export let whereInsert: 'before' | 'after' = 'after'
|
||||
export let deleteBranch: { module: FlowModule; index: number } | undefined = undefined
|
||||
export let id: string | undefined = undefined
|
||||
export let moving: string | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
insert: {
|
||||
detail: 'script' | 'forloop' | 'branchone' | 'branchall' | 'trigger'
|
||||
detail: 'script' | 'forloop' | 'branchone' | 'branchall' | 'trigger' | 'move'
|
||||
modules: FlowModule[]
|
||||
index: number
|
||||
}
|
||||
@@ -87,15 +88,30 @@
|
||||
? 'top-12'
|
||||
: '-top-10'} left-[50%] right-[50%] -translate-x-1/2"
|
||||
>
|
||||
<InsertModuleButton
|
||||
bind:open={openMenu}
|
||||
trigger={label == 'Input'}
|
||||
on:new={(e) => {
|
||||
if (modules) {
|
||||
dispatch('insert', { modules, index, detail: e.detail })
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{#if moving}
|
||||
<button
|
||||
title="Add branch"
|
||||
on:click={() => {
|
||||
if (modules) {
|
||||
dispatch('insert', { modules, index, detail: 'move' })
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
class=" text-gray-900 bg-white border mx-0.5 border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 font-medium rounded-full text-sm w-6 h-6 flex items-center justify-center"
|
||||
>
|
||||
<ClipboardCopy size={12} />
|
||||
</button>
|
||||
{:else}
|
||||
<InsertModuleButton
|
||||
bind:open={openMenu}
|
||||
trigger={label == 'Input'}
|
||||
on:new={(e) => {
|
||||
if (modules) {
|
||||
dispatch('insert', { modules, index, detail: e.detail })
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { FlowModule } from '$lib/gen'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import type ScriptEditorDrawer from './content/ScriptEditorDrawer.svelte'
|
||||
import type { Schedule } from './scheduleUtils'
|
||||
|
||||
export type FlowEditorContext = {
|
||||
selectedId: Writable<string>
|
||||
moving: Writable<{ module: FlowModule, modules: FlowModule[] } | undefined>
|
||||
schedule: Writable<Schedule>,
|
||||
previewArgs: Writable<Record<string, any>>,
|
||||
scriptEditorDrawer: Writable<ScriptEditorDrawer | undefined>
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
export let initialZoom: number | undefined = 1.0
|
||||
|
||||
export let insertable = false
|
||||
export let moving: string | undefined = undefined
|
||||
|
||||
setContext<{ selectedId: Writable<string | undefined> }>('FlowGraphContext', { selectedId })
|
||||
|
||||
@@ -50,6 +51,7 @@
|
||||
|
||||
$: {
|
||||
rebuildOnChange
|
||||
moving
|
||||
width && height && minHeight && $selectedId && flowModuleStates
|
||||
nodes = edges = []
|
||||
errorHandlers = {}
|
||||
@@ -246,7 +248,8 @@
|
||||
branchable,
|
||||
bgColor: getStateColor(flowModuleStates?.[mod.id]?.type),
|
||||
annotation,
|
||||
modules
|
||||
modules,
|
||||
moving
|
||||
},
|
||||
cb: (e: string, detail: any) => {
|
||||
if (e == 'delete') {
|
||||
@@ -262,10 +265,8 @@
|
||||
dispatch('insert', detail)
|
||||
} else if (e == 'newBranch') {
|
||||
dispatch('newBranch', detail)
|
||||
} else if (e == 'movefrom') {
|
||||
dispatch('moveto', detail)
|
||||
} else if (e == 'moveto') {
|
||||
dispatch('movefrom', detail)
|
||||
} else if (e == 'move') {
|
||||
dispatch('move', { module: mod, modules })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -576,7 +577,8 @@
|
||||
selectable,
|
||||
whereInsert,
|
||||
deleteBranch,
|
||||
id: mid
|
||||
id: mid,
|
||||
moving
|
||||
},
|
||||
cb: (e: string, detail: any) => {
|
||||
if (e == 'insert') {
|
||||
|
||||
Reference in New Issue
Block a user