feat(frontend): add ability to move nodes

This commit is contained in:
Ruben Fiszel
2023-03-06 18:41:20 +01:00
parent d7aa565ea8
commit 392b9d5ec3
7 changed files with 206 additions and 155 deletions
@@ -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') {