mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-21 16:02:28 +00:00
feat: improve app connection UX #4687
* Add animation on connection plugs * Remove connection pannel * Modify click outside * fix minor issue * remove unused component * Prevent keyboard component navigation when connecting * fix left right panel inversion * fix pannel logic * close secondary menu on connection * Adjust plug position according to id badge size * Change components colors * Change plug color * Fix escape connection * Change cursor when connecting * Add user toast on connecting * Add component to exclusion area in connection mode * exit connection on click connection button * create connection button component * remove debug logs * fix pen color * polish * Add alert message in connection mode * fix minor issue * fix unwanted cursor override
This commit is contained in:
Vendored
+1
@@ -1,5 +1,6 @@
|
||||
declare namespace svelte.JSX {
|
||||
interface DOMAttributes<T> {
|
||||
onclick_outside?: CompositionEventHandler<T>
|
||||
onpointerdown_outside?: (event: CustomEvent) => void
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,13 +37,11 @@
|
||||
import { findGridItem, findGridItemParentGrid } from './appUtils'
|
||||
import ComponentNavigation from './component/ComponentNavigation.svelte'
|
||||
import CssSettings from './componentsPanel/CssSettings.svelte'
|
||||
import ConnectionInstructions from './ConnectionInstructions.svelte'
|
||||
import SettingsPanel from './SettingsPanel.svelte'
|
||||
import {
|
||||
SecondaryMenu,
|
||||
secondaryMenuLeft,
|
||||
secondaryMenuLeftStore,
|
||||
secondaryMenuRight,
|
||||
secondaryMenuRightStore
|
||||
} from './settingsPanel/secondaryMenu'
|
||||
import Popover from '../../Popover.svelte'
|
||||
@@ -294,15 +292,6 @@
|
||||
hasResult: writable<Record<string, boolean>>({})
|
||||
})
|
||||
|
||||
$: if ($connectingInput.opened) {
|
||||
secondaryMenuRight.open(ConnectionInstructions, {}, () => {
|
||||
$connectingInput.opened = false
|
||||
})
|
||||
secondaryMenuLeft.close()
|
||||
} else {
|
||||
secondaryMenuRight.close()
|
||||
}
|
||||
|
||||
function onThemeChange() {
|
||||
$darkMode = document.documentElement.classList.contains('dark')
|
||||
}
|
||||
@@ -324,28 +313,6 @@
|
||||
let toggled = false
|
||||
let cssToggled = false
|
||||
|
||||
$: if ($connectingInput.opened && !toggled) {
|
||||
tmpRunnablePanelSize = runnablePanelSize
|
||||
tmpGridPanelSize = gridPanelSize
|
||||
|
||||
animateTo(runnablePanelSize, 0, (newValue: number) => (runnablePanelSize = newValue))
|
||||
animateTo(gridPanelSize, 100, (newValue: number) => (gridPanelSize = newValue))
|
||||
|
||||
toggled = true
|
||||
} else if (!$connectingInput.opened && toggled) {
|
||||
animateTo(
|
||||
runnablePanelSize,
|
||||
tmpRunnablePanelSize,
|
||||
(newValue: number) => (runnablePanelSize = newValue)
|
||||
)
|
||||
animateTo(gridPanelSize, tmpGridPanelSize, (newValue: number) => (gridPanelSize = newValue))
|
||||
|
||||
tmpRunnablePanelSize = -1
|
||||
tmpGridPanelSize = -1
|
||||
|
||||
toggled = false
|
||||
}
|
||||
|
||||
// Animation logic for cssInput
|
||||
$: animateCssInput($cssEditorOpen)
|
||||
$: $cssEditorOpen &&
|
||||
@@ -546,27 +513,58 @@
|
||||
|
||||
let centerPanelWidth = 0
|
||||
|
||||
function hideLeftPanel() {
|
||||
function hideLeftPanel(animate: boolean = false) {
|
||||
storedLeftPanelSize = leftPanelSize
|
||||
leftPanelSize = 0
|
||||
if (animate) {
|
||||
animateTo(leftPanelSize, 0, (newValue: number) => (leftPanelSize = newValue))
|
||||
} else {
|
||||
leftPanelSize = 0
|
||||
}
|
||||
centerPanelSize = centerPanelSize + storedLeftPanelSize
|
||||
if ($connectingInput.opened) {
|
||||
$connectingInput.opened = false
|
||||
}
|
||||
}
|
||||
|
||||
function hideRightPanel() {
|
||||
storedRightPanelSize = rightPanelSize
|
||||
rightPanelSize = 0
|
||||
centerPanelSize = centerPanelSize + storedRightPanelSize
|
||||
if ($connectingInput.opened) {
|
||||
$connectingInput.opened = false
|
||||
}
|
||||
}
|
||||
|
||||
function hideBottomPanel() {
|
||||
function hideBottomPanel(animate: boolean = false) {
|
||||
if (runnablePanelSize === 0) {
|
||||
return
|
||||
}
|
||||
storedBottomPanelSize = runnablePanelSize
|
||||
gridPanelSize = 99
|
||||
runnablePanelSize = 0
|
||||
if (animate) {
|
||||
tmpRunnablePanelSize = runnablePanelSize
|
||||
tmpGridPanelSize = gridPanelSize
|
||||
|
||||
animateTo(runnablePanelSize, 0, (newValue: number) => (runnablePanelSize = newValue))
|
||||
animateTo(gridPanelSize, 100, (newValue: number) => (gridPanelSize = newValue))
|
||||
} else {
|
||||
runnablePanelSize = 0
|
||||
gridPanelSize = 99
|
||||
}
|
||||
}
|
||||
|
||||
function showLeftPanel() {
|
||||
leftPanelSize = storedLeftPanelSize
|
||||
centerPanelSize = centerPanelSize - storedLeftPanelSize
|
||||
function showLeftPanel(animate: boolean = false) {
|
||||
if (leftPanelSize !== 0) {
|
||||
return
|
||||
}
|
||||
if (animate) {
|
||||
animateTo(
|
||||
leftPanelSize,
|
||||
storedLeftPanelSize,
|
||||
(newValue: number) => (leftPanelSize = newValue)
|
||||
)
|
||||
} else {
|
||||
leftPanelSize = storedLeftPanelSize
|
||||
}
|
||||
storedLeftPanelSize = 0
|
||||
}
|
||||
|
||||
@@ -576,9 +574,25 @@
|
||||
storedRightPanelSize = 0
|
||||
}
|
||||
|
||||
function showBottomPanel() {
|
||||
runnablePanelSize = storedBottomPanelSize
|
||||
gridPanelSize = gridPanelSize - storedBottomPanelSize
|
||||
function showBottomPanel(animate: boolean = false) {
|
||||
if (runnablePanelSize !== 0) {
|
||||
return
|
||||
}
|
||||
if (animate) {
|
||||
animateTo(
|
||||
runnablePanelSize,
|
||||
storedBottomPanelSize,
|
||||
(newValue: number) => (runnablePanelSize = newValue)
|
||||
)
|
||||
animateTo(
|
||||
gridPanelSize,
|
||||
gridPanelSize - storedBottomPanelSize,
|
||||
(newValue: number) => (gridPanelSize = newValue)
|
||||
)
|
||||
} else {
|
||||
runnablePanelSize = storedBottomPanelSize
|
||||
gridPanelSize = gridPanelSize - storedBottomPanelSize
|
||||
}
|
||||
storedBottomPanelSize = 0
|
||||
}
|
||||
|
||||
@@ -630,10 +644,56 @@
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'Escape': {
|
||||
if ($connectingInput.opened) {
|
||||
$connectingInput.opened = false
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let previousLeftPanelHidden: boolean = false
|
||||
let previousBottomPanelHidden: boolean = false
|
||||
|
||||
async function updatePannelInConnecting() {
|
||||
if ($connectingInput.opened && !toggled) {
|
||||
previousLeftPanelHidden = leftPanelSize === 0
|
||||
previousBottomPanelHidden = runnablePanelSize === 0
|
||||
if (previousLeftPanelHidden) {
|
||||
showLeftPanel(true)
|
||||
}
|
||||
if (!previousBottomPanelHidden) {
|
||||
hideBottomPanel(true)
|
||||
}
|
||||
secondaryMenuLeft.close()
|
||||
toggled = true
|
||||
} else if (!$connectingInput.opened && toggled) {
|
||||
if (previousLeftPanelHidden) {
|
||||
hideLeftPanel(true)
|
||||
}
|
||||
if (!previousBottomPanelHidden) {
|
||||
showBottomPanel(true)
|
||||
}
|
||||
toggled = false
|
||||
}
|
||||
}
|
||||
|
||||
$: $connectingInput.opened, updatePannelInConnecting()
|
||||
|
||||
let testJob: Job | undefined = undefined
|
||||
let jobToWatch: { componentId: string; job: string } | undefined = undefined
|
||||
|
||||
$: updateCursorStyle(!!$connectingInput.opened)
|
||||
|
||||
function updateCursorStyle(disabled: boolean) {
|
||||
if (disabled) {
|
||||
document.documentElement.style.setProperty('--global-cursor', 'not-allowed', 'important')
|
||||
} else {
|
||||
document.documentElement.style.removeProperty('--global-cursor')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -704,6 +764,12 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $connectingInput.opened}
|
||||
<div class="absolute z-50 inset-0 w-min h-min whitespace-nowrap mx-auto pt-0.5">
|
||||
<Alert title="Press Esc to exit connection mode." size="xs" class="h-10 py-1" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<SplitPanesWrapper>
|
||||
<Splitpanes id="o1" class="max-w-full overflow-hidden">
|
||||
<Pane bind:size={leftPanelSize} minSize={5} maxSize={33}>
|
||||
@@ -739,7 +805,7 @@
|
||||
<div class="absolute top-0.5 left-0.5 z-50">
|
||||
<HideButton
|
||||
on:click={() => showLeftPanel()}
|
||||
direction="right"
|
||||
direction="left"
|
||||
hidden
|
||||
btnClasses="border bg-surface"
|
||||
/>
|
||||
@@ -749,7 +815,7 @@
|
||||
<div class="absolute top-0.5 right-0.5 z-50">
|
||||
<HideButton
|
||||
on:click={() => showRightPanel()}
|
||||
direction="left"
|
||||
direction="right"
|
||||
hidden
|
||||
btnClasses="border bg-surface"
|
||||
/>
|
||||
@@ -811,7 +877,9 @@
|
||||
style={$componentActive ? `top: -${$yTop}px;` : ''}
|
||||
>
|
||||
{#if $appStore.grid}
|
||||
<ComponentNavigation />
|
||||
{#if !$connectingInput?.opened}
|
||||
<ComponentNavigation />
|
||||
{/if}
|
||||
|
||||
<div
|
||||
on:pointerdown|stopPropagation
|
||||
@@ -948,14 +1016,7 @@
|
||||
</Tab>
|
||||
</Popover>
|
||||
<div class="h-full w-full flex justify-end px-1">
|
||||
<HideButton
|
||||
on:click={() => {
|
||||
storedRightPanelSize = rightPanelSize
|
||||
rightPanelSize = 0
|
||||
centerPanelSize = centerPanelSize + storedRightPanelSize
|
||||
}}
|
||||
direction="right"
|
||||
/>
|
||||
<HideButton on:click={() => hideRightPanel()} direction="right" />
|
||||
</div>
|
||||
<div slot="content" class="h-full overflow-y-auto">
|
||||
<TabContent class="overflow-auto h-full" value="settings">
|
||||
@@ -1034,4 +1095,13 @@
|
||||
#o2 > .splitpanes__pane {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* Conditionally apply the disabled cursor globally */
|
||||
:global(*) {
|
||||
cursor: var(--element-cursor, var(--global-cursor, auto));
|
||||
}
|
||||
|
||||
:global(.connection-access) {
|
||||
--element-cursor: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils'
|
||||
import type { AppViewerContext } from '../types'
|
||||
import { Anchor, ArrowDownFromLine, Bug, Network, Pen, Plug2 } from 'lucide-svelte'
|
||||
import { Anchor, ArrowDownFromLine, Bug, Network, Pen, Plug } from 'lucide-svelte'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
import { Button, Popup } from '$lib/components/common'
|
||||
@@ -12,6 +12,7 @@
|
||||
import TabsDebug from './TabsDebug.svelte'
|
||||
import ComponentOutputViewer from './contextPanel/ComponentOutputViewer.svelte'
|
||||
import DecisionTreeDebug from './DecisionTreeDebug.svelte'
|
||||
import AnimatedButton from '$lib/components/common/button/AnimatedButton.svelte'
|
||||
|
||||
export let component: AppComponent
|
||||
export let selected: boolean
|
||||
@@ -33,6 +34,7 @@
|
||||
let maxWidth = 10
|
||||
let isManuallySelected = false
|
||||
let componentIsDebugging = false
|
||||
let id_width = 0
|
||||
|
||||
$: maxWidth = Math.max(Math.round(0.2 * componentContainerWidth), MINIMUM_WIDTH)
|
||||
|
||||
@@ -59,15 +61,27 @@
|
||||
</script>
|
||||
|
||||
{#if connecting}
|
||||
<div class="absolute z-50 left-6 -top-[11px] overflow-auto">
|
||||
<div
|
||||
class="absolute z-50 overflow-auto -top-[18px] connection-access"
|
||||
style="left: {id_width}px;"
|
||||
data-connection-button
|
||||
>
|
||||
<Popup floatingConfig={{ strategy: 'fixed', placement: 'bottom-start' }}>
|
||||
<svelte:fragment slot="button">
|
||||
<button
|
||||
id={`connect-output-${component.id}`}
|
||||
class="bg-red-500/70 border border-red-600 px-1 py-0.5"
|
||||
title="Outputs"
|
||||
aria-label="Open output"><Plug2 size={12} /></button
|
||||
<AnimatedButton
|
||||
animate={true}
|
||||
baseRadius="9999px"
|
||||
wrapperClasses="h-full w-full pt-2"
|
||||
marginWidth="2px"
|
||||
animationDuration="3s"
|
||||
>
|
||||
<button
|
||||
id={`connect-output-${component.id}`}
|
||||
class="h-[20px] w-[20px] bg-surface rounded-full center-center text-primary"
|
||||
title="Outputs"
|
||||
aria-label="Open output"><Plug size={12} /></button
|
||||
>
|
||||
</AnimatedButton>
|
||||
</svelte:fragment>
|
||||
<ComponentOutputViewer
|
||||
suffix="connect"
|
||||
@@ -91,15 +105,19 @@
|
||||
draggable="false"
|
||||
title={`Id: ${component.id}`}
|
||||
class={twMerge(
|
||||
'py-0.5 text-2xs w-fit h-full min-h-5 border rounded z-50 cursor-move flex flex-row flex-nowrap font-semibold items-center shadow',
|
||||
'py-0.5 text-2xs w-fit h-full min-h-5 rounded z-50 cursor-move flex flex-row flex-nowrap font-semibold items-center shadow',
|
||||
selected
|
||||
? 'bg-indigo-500/90 border-indigo-500 text-white'
|
||||
? 'bg-blue-600/90 text-white'
|
||||
: $connectingInput.opened
|
||||
? 'bg-red-500/90 border-red-600 text-white'
|
||||
: 'bg-blue-500/90 border-blue-600 text-white'
|
||||
? 'bg-[#f8aa4b]/90 text-white'
|
||||
: 'bg-blue-400/90 text-white'
|
||||
)}
|
||||
>
|
||||
<div class={`px-1 text-2xs w-full min-w-4 h-full truncate`} style="max-width: {maxWidth}px;">
|
||||
<div
|
||||
class={`px-1 text-2xs w-full min-w-4 h-full truncate`}
|
||||
style="max-width: {maxWidth}px;"
|
||||
bind:clientWidth={id_width}
|
||||
>
|
||||
{component.id}
|
||||
</div>
|
||||
{#if !connecting}
|
||||
@@ -109,8 +127,8 @@
|
||||
class={twMerge(
|
||||
'px-1 py-0.5 text-2xs font-bold rounded cursor-pointer w-fit h-full',
|
||||
fullHeight
|
||||
? ' bg-indigo-800 text-indigo-200'
|
||||
: 'text-white hover:bg-indigo-700 hover:text-indigo-200'
|
||||
? 'bg-blue-300 text-blue-800'
|
||||
: 'text-white hover:bg-blue-400 hover:text-white'
|
||||
)}
|
||||
on:click={() => dispatch('fillHeight')}
|
||||
on:pointerdown|stopPropagation
|
||||
@@ -122,9 +140,7 @@
|
||||
title="Lock Position"
|
||||
class={twMerge(
|
||||
'px-1 py-0.5 text-2xs font-bold rounded cursor-pointer w-fit h-full',
|
||||
locked
|
||||
? ' bg-indigo-800 text-indigo-200'
|
||||
: 'text-white hover:bg-indigo-700 hover:text-indigo-200'
|
||||
locked ? 'bg-blue-300 text-blue-800' : 'text-white hover:bg-blue-400 hover:text-white'
|
||||
)}
|
||||
on:click={() => dispatch('lock')}
|
||||
on:pointerdown|stopPropagation
|
||||
@@ -141,10 +157,10 @@
|
||||
{#if selected && !connecting && checkComponentOptions()}
|
||||
<div
|
||||
class={twMerge(
|
||||
'px-1 py-0.5 text-2xs font-semibold w-fit min-h-5 border shadow rounded z-50 flex flex-row items-center flex-nowrap',
|
||||
'px-1 py-0.5 text-2xs font-semibold w-fit min-h-5 shadow rounded z-50 flex flex-row items-center flex-nowrap',
|
||||
isManuallySelected || componentIsDebugging
|
||||
? 'bg-red-100 text-red-600 border-red-500'
|
||||
: 'bg-indigo-100/90 border-indigo-200 text-indigo-600'
|
||||
: 'bg-blue-100/90 border-blue-200 text-blue-600'
|
||||
)}
|
||||
>
|
||||
{#if hasInlineEditor}
|
||||
@@ -153,17 +169,13 @@
|
||||
class={twMerge(
|
||||
'px-1 py-0.5 text-2xs font-bold rounded cursor-pointer w-fit h-full',
|
||||
inlineEditorOpened
|
||||
? 'bg-indigo-300 text-indigo-800'
|
||||
: 'text-indigo-600 hover:bg-indigo-300 hover:text-indigo-800'
|
||||
? 'bg-blue-300 text-blue-800'
|
||||
: 'text-blue-600 hover:bg-blue-300 hover:text-blue-800'
|
||||
)}
|
||||
on:click={() => dispatch('triggerInlineEditor')}
|
||||
on:pointerdown|stopPropagation
|
||||
>
|
||||
{#if inlineEditorOpened}
|
||||
<Pen aria-label="Unlock position" size={11} />
|
||||
{:else}
|
||||
<Pen aria-label="Lock position" size={11} />
|
||||
{/if}
|
||||
<Pen aria-label="Edit" size={11} />
|
||||
</button>
|
||||
{/if}
|
||||
{#if component.type === 'conditionalwrapper'}
|
||||
@@ -185,10 +197,10 @@
|
||||
<button
|
||||
title={'Open Decision Tree Editor'}
|
||||
class={twMerge(
|
||||
'px-1 py-0.5 text-2xs font-bold rounded cursor-pointer w-fit h-full',
|
||||
'px-1 py-0.5 text-2xs font-bold rounded cursor-pointer w-fit h-full center-center',
|
||||
componentIsDebugging
|
||||
? 'text-red-600 hover:bg-red-300 hover:text-red-800'
|
||||
: 'text-indigo-600 hover:bg-indigo-300 hover:text-indigo-800'
|
||||
: 'text-blue-600 hover:bg-blue-300 hover:text-blue-800'
|
||||
)}
|
||||
on:click={() => {
|
||||
const element = document.getElementById(`decision-tree-graph-editor`)
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Alert, Button } from '$lib/components/common'
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppViewerContext } from '../types'
|
||||
import { secondaryMenuRight } from './settingsPanel/secondaryMenu'
|
||||
import { Plug2 } from 'lucide-svelte'
|
||||
|
||||
const { connectingInput } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
function stopConnecting() {
|
||||
$connectingInput.opened = false
|
||||
$connectingInput.input = undefined
|
||||
|
||||
secondaryMenuRight.close()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="m-2">
|
||||
<Alert title="Connecting" type="info">
|
||||
<div class="flex gap-2 flex-col">
|
||||
<div>
|
||||
Click on the output of the component you want to connect to on the left panel 'Outputs' or
|
||||
in the popup by clicking the <span
|
||||
><div class="inline-flex"
|
||||
><div class="bg-red-500/90 border-red-600 px-1 py-0.5"><Plug2 size={12} /></div></div
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<Button color="blue" variant="border" size="xs" on:click={stopConnecting}>
|
||||
Stop connecting
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Alert>
|
||||
</div>
|
||||
@@ -78,7 +78,7 @@
|
||||
'px-1 text-2xs font-bold rounded cursor-pointer w-fit h-full',
|
||||
componentIsDebugging
|
||||
? ' hover:bg-red-300 hover:text-red-800'
|
||||
: ' hover:bg-indigo-300 hover:text-indigo-800'
|
||||
: 'text-blue-600 hover:bg-blue-300 hover:text-blue-800'
|
||||
)}
|
||||
on:click={() => dispatch('triggerInlineEditor')}
|
||||
on:pointerdown|stopPropagation
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
'text-2xs font-bold w-fit h-full cursor-pointer rounded',
|
||||
isManuallySelected
|
||||
? 'hover:bg-red-200 hover:text-red-800'
|
||||
: ' hover:text-indigo-800 hover:bg-indigo-300'
|
||||
: 'text-blue-600 hover:bg-blue-300 hover:text-blue-800'
|
||||
)}
|
||||
on:click={() => dispatch('triggerInlineEditor')}
|
||||
on:pointerdown|stopPropagation
|
||||
|
||||
@@ -912,6 +912,7 @@ export function connectInput(
|
||||
if (connectingInput) {
|
||||
if (connectingInput.onConnect) {
|
||||
connectingInput.onConnect({ componentId, path })
|
||||
sendUserToast(`Connected to ${componentId}.${path}`, false)
|
||||
}
|
||||
|
||||
connectingInput = {
|
||||
|
||||
@@ -166,10 +166,11 @@
|
||||
}}
|
||||
on:mouseout|stopPropagation={mouseOut}
|
||||
class={twMerge(
|
||||
'h-full flex flex-col w-full component relative',
|
||||
'h-full flex flex-col w-full component relative connection-access',
|
||||
initializing ? 'overflow-hidden h-0' : '',
|
||||
hidden && $mode === 'preview' ? 'hidden' : ''
|
||||
)}
|
||||
data-connection-button
|
||||
>
|
||||
{#if locked && componentActive && $componentActive && moveMode === 'move' && componentDraggedId && componentDraggedId !== component.id && cachedAreOnTheSameSubgrid}
|
||||
<div
|
||||
@@ -237,10 +238,10 @@
|
||||
$mode === 'dnd' ? 'bg-surface/40' : '',
|
||||
$hoverStore === component.id && $mode !== 'preview'
|
||||
? $connectingInput.opened
|
||||
? 'outline outline-orange-600'
|
||||
: 'outline outline-blue-600'
|
||||
? 'outline outline-[#f8aa4b]'
|
||||
: 'outline outline-blue-400'
|
||||
: '',
|
||||
selected && $mode !== 'preview' ? 'outline outline-indigo-600' : '',
|
||||
selected && $mode !== 'preview' ? 'outline outline-blue-600' : '',
|
||||
$mode != 'preview' ? 'cursor-pointer' : '',
|
||||
'relative z-auto',
|
||||
$app.css?.['app']?.['component']?.class,
|
||||
|
||||
@@ -41,12 +41,14 @@
|
||||
|
||||
{#if object != undefined && Object.keys(object).length > 0}
|
||||
{#if $hasResult[componentId] || $search == ''}
|
||||
<div class="pl-2">
|
||||
<div class="pl-2 !cursor-pointer connection-access" data-connection-button>
|
||||
<ObjectViewer
|
||||
json={filtered}
|
||||
on:select
|
||||
topBrackets={false}
|
||||
pureViewer={!$connectingInput.opened}
|
||||
allowCopy={!$connectingInput.opened}
|
||||
connecting={$connectingInput.opened}
|
||||
/>
|
||||
</div>
|
||||
{:else if $search.length > 0}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
|
||||
import type { AppViewerContext, ContextPanelContext } from '../../types'
|
||||
@@ -12,6 +11,8 @@
|
||||
import { ClearableInput } from '../../../common'
|
||||
import DocLink from '../settingsPanel/DocLink.svelte'
|
||||
import HideButton from '../settingsPanel/HideButton.svelte'
|
||||
import AnimatedButton from '$lib/components/common/button/AnimatedButton.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
const { connectingInput, app } = getContext<AppViewerContext>('AppViewerContext')
|
||||
const { search } = getContext<ContextPanelContext>('ContextPanel')
|
||||
@@ -33,60 +34,67 @@
|
||||
<DocLink docLink="https://www.windmill.dev/docs/apps/outputs" />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<div
|
||||
class={classNames(
|
||||
'bg-surface w-full h-full z-30 overflow-auto',
|
||||
$connectingInput.opened
|
||||
? 'border-blue-500 border-t-2 border-r-2 bg-blue-50/50 dark:bg-frost-900/50 z-50'
|
||||
: ''
|
||||
)}
|
||||
<AnimatedButton
|
||||
animate={$connectingInput.opened}
|
||||
baseRadius="0px"
|
||||
wrapperClasses="h-full w-full pt-2"
|
||||
marginWidth="4px"
|
||||
animationDuration="2s"
|
||||
>
|
||||
<div class="min-w-[150px]">
|
||||
<div class="sticky z-10 top-0 left-0 w-full p-1.5 bg-surface">
|
||||
<ClearableInput bind:value={$search} placeholder="Search outputs..." />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<span class="text-xs font-semibold text-secondary p-2">State & Context</span>
|
||||
|
||||
<OutputHeader selectable={false} id={'ctx'} name={'App Context'} first color="blue">
|
||||
<ComponentOutputViewer
|
||||
componentId={'ctx'}
|
||||
on:select={({ detail }) => {
|
||||
$connectingInput = connectInput($connectingInput, 'ctx', detail)
|
||||
}}
|
||||
/>
|
||||
</OutputHeader>
|
||||
|
||||
<OutputHeader
|
||||
selectable={false}
|
||||
id={'state'}
|
||||
name={'State'}
|
||||
color="blue"
|
||||
disabled={!hasState}
|
||||
>
|
||||
<ComponentOutputViewer
|
||||
bind:hasContent={hasState}
|
||||
componentId={'state'}
|
||||
on:select={({ detail }) => {
|
||||
$connectingInput = connectInput($connectingInput, 'state', detail)
|
||||
}}
|
||||
/>
|
||||
</OutputHeader>
|
||||
<div
|
||||
class={twMerge(
|
||||
'bg-surface w-full h-full z-30 overflow-auto connection-access',
|
||||
$connectingInput.opened ? 'z-50 dark:bg-frost-900' : ''
|
||||
)}
|
||||
data-connection-button
|
||||
>
|
||||
<div class="min-w-[150px]">
|
||||
<div class="sticky z-10 top-0 left-0 w-full p-1.5 bg-surface">
|
||||
<ClearableInput bind:value={$search} placeholder="Search outputs..." />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="text-xs font-semibold text-secondary p-2">Components</span>
|
||||
{#each $app.grid as gridItem, index (gridItem.id)}
|
||||
<ComponentOutput {gridItem} first={index === 0} />
|
||||
{/each}
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-xs font-semibold text-secondary p-2">Background runnables</span>
|
||||
<BackgroundScriptsOutput />
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<span class="text-xs font-semibold text-secondary p-2">State & Context</span>
|
||||
|
||||
<OutputHeader selectable={false} id={'ctx'} name={'App Context'} first color="blue">
|
||||
<ComponentOutputViewer
|
||||
componentId={'ctx'}
|
||||
on:select={({ detail }) => {
|
||||
$connectingInput = connectInput($connectingInput, 'ctx', detail)
|
||||
}}
|
||||
/>
|
||||
</OutputHeader>
|
||||
|
||||
<OutputHeader
|
||||
selectable={false}
|
||||
id={'state'}
|
||||
name={'State'}
|
||||
color="blue"
|
||||
disabled={!hasState}
|
||||
>
|
||||
<ComponentOutputViewer
|
||||
bind:hasContent={hasState}
|
||||
componentId={'state'}
|
||||
on:select={({ detail }) => {
|
||||
$connectingInput = connectInput($connectingInput, 'state', detail)
|
||||
}}
|
||||
/>
|
||||
</OutputHeader>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="text-xs font-semibold text-secondary p-2">Components</span>
|
||||
{#each $app.grid as gridItem, index (gridItem.id)}
|
||||
<ComponentOutput {gridItem} first={index === 0} />
|
||||
{/each}
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-xs font-semibold text-secondary p-2">Background runnables</span>
|
||||
<BackgroundScriptsOutput />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AnimatedButton>
|
||||
</PanelSection>
|
||||
|
||||
@@ -219,7 +219,7 @@
|
||||
$selectedComponent?.includes(id)
|
||||
? openBackground[color]
|
||||
: $connectingInput.hoveredComponent === id
|
||||
? 'bg-orange-300 '
|
||||
? 'bg-[#fab157]'
|
||||
: 'bg-surface-secondary',
|
||||
first ? 'border-t' : '',
|
||||
nested ? 'border-l' : '',
|
||||
|
||||
+20
-14
@@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import { getContext, createEventDispatcher } from 'svelte'
|
||||
import type { AppInput, InputConnection } from '../../inputType'
|
||||
import type { AppViewerContext } from '../../types'
|
||||
import { Code, CurlyBraces, FunctionSquare, Pen, Plug, Plug2 } from 'lucide-svelte'
|
||||
import { Code, CurlyBraces, FunctionSquare, Pen, Plug2 } from 'lucide-svelte'
|
||||
|
||||
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
|
||||
import { Button } from '$lib/components/common'
|
||||
import ConnectionButton from '$lib/components/common/button/ConnectionButton.svelte'
|
||||
import type EvalV2InputEditor from './inputEditor/EvalV2InputEditor.svelte'
|
||||
|
||||
export let componentInput: AppInput
|
||||
@@ -15,6 +15,8 @@
|
||||
|
||||
const { onchange, connectingInput, app } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: if (componentInput.fieldType == 'template' && componentInput.type == 'static') {
|
||||
//@ts-ignore
|
||||
componentInput.type = 'templatev2'
|
||||
@@ -47,7 +49,7 @@
|
||||
|
||||
{#if componentInput.fieldType !== 'any'}
|
||||
<div class="w-full">
|
||||
<div class="mx-auto flex gap-2" bind:clientWidth>
|
||||
<div class="flex gap-2 justify-end" bind:clientWidth>
|
||||
<ToggleButtonGroup
|
||||
on:selected={() => {
|
||||
onchange?.()
|
||||
@@ -105,14 +107,19 @@
|
||||
id="data-source-compute"
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
|
||||
<div class="flex">
|
||||
<Button
|
||||
size="xs"
|
||||
variant="border"
|
||||
color="light"
|
||||
title="Connect"
|
||||
id={`plug`}
|
||||
on:click={() => {
|
||||
<ConnectionButton
|
||||
closeConnection={() => {
|
||||
$connectingInput = {
|
||||
opened: false,
|
||||
hoveredComponent: undefined,
|
||||
input: undefined,
|
||||
onConnect: () => {}
|
||||
}
|
||||
dispatch('select', true)
|
||||
}}
|
||||
openConnection={() => {
|
||||
$connectingInput = {
|
||||
opened: true,
|
||||
input: undefined,
|
||||
@@ -120,9 +127,8 @@
|
||||
onConnect: applyConnection
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Plug size={12} />
|
||||
</Button>
|
||||
isOpen={!!$connectingInput.opened}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { addWhitespaceBeforeCapitals, capitalize, classNames } from '$lib/utils'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
|
||||
import ConnectedInputEditor from './inputEditor/ConnectedInputEditor.svelte'
|
||||
import EvalInputEditor from './inputEditor/EvalInputEditor.svelte'
|
||||
import RowInputEditor from './inputEditor/RowInputEditor.svelte'
|
||||
@@ -12,10 +11,11 @@
|
||||
import type { InputConnection, InputType, UploadAppInput } from '../../inputType'
|
||||
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
|
||||
import { FunctionSquare, Loader2, Pen, Plug, Plug2, Upload, User } from 'lucide-svelte'
|
||||
import { FunctionSquare, Loader2, Pen, Plug2, Upload, User } from 'lucide-svelte'
|
||||
import { fieldTypeToTsType } from '../../utils'
|
||||
import EvalV2InputEditor from './inputEditor/EvalV2InputEditor.svelte'
|
||||
import { Button } from '$lib/components/common'
|
||||
import ConnectionButton from '$lib/components/common/button/ConnectionButton.svelte'
|
||||
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
|
||||
export let id: string
|
||||
@@ -69,6 +69,24 @@
|
||||
value: undefined
|
||||
}
|
||||
}
|
||||
|
||||
function closeConnection() {
|
||||
$connectingInput = {
|
||||
opened: false,
|
||||
hoveredComponent: undefined,
|
||||
input: undefined,
|
||||
onConnect: () => {}
|
||||
}
|
||||
}
|
||||
|
||||
function openConnection() {
|
||||
$connectingInput = {
|
||||
opened: true,
|
||||
input: undefined,
|
||||
hoveredComponent: undefined,
|
||||
onConnect: applyConnection
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !(resourceOnly && (fieldType !== 'object' || !format?.startsWith('resource-')))}
|
||||
@@ -135,25 +153,7 @@
|
||||
{/if}
|
||||
<ToggleButton value="evalv2" icon={FunctionSquare} iconOnly tooltip="Eval" />
|
||||
</ToggleButtonGroup>
|
||||
<div>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="border"
|
||||
color="light"
|
||||
title="Connect"
|
||||
on:click={() => {
|
||||
$connectingInput = {
|
||||
opened: true,
|
||||
input: undefined,
|
||||
hoveredComponent: undefined,
|
||||
onConnect: applyConnection
|
||||
}
|
||||
}}
|
||||
id="schema-plug"
|
||||
>
|
||||
<Plug size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
<ConnectionButton {closeConnection} {openConnection} isOpen={!!$connectingInput.opened} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
-9
@@ -1,23 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { fly } from 'svelte/transition'
|
||||
import { secondaryMenuLeft, secondaryMenuRight } from './'
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppViewerContext } from '../../../types'
|
||||
import CloseButton from '$lib/components/common/CloseButton.svelte'
|
||||
import { zIndexes } from '$lib/zIndexes'
|
||||
import DocLink from '../DocLink.svelte'
|
||||
|
||||
const { selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
|
||||
export let right: boolean
|
||||
|
||||
let secondaryMenu = right ? secondaryMenuRight : secondaryMenuLeft
|
||||
let width: number
|
||||
let lastSelected = $selectedComponent
|
||||
|
||||
$: if (right && lastSelected !== $selectedComponent) {
|
||||
secondaryMenu.close()
|
||||
lastSelected = $selectedComponent
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<script lang="ts">
|
||||
import { Plug } from 'lucide-svelte'
|
||||
import { clickOutside, pointerDownOutside } from '$lib/utils'
|
||||
import AnimatedButton from '$lib/components/common/button/AnimatedButton.svelte'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
|
||||
export let isOpen = false
|
||||
export let openConnection: () => void
|
||||
export let closeConnection: () => void
|
||||
|
||||
let selected = false
|
||||
|
||||
async function getConnectionButtonElements(): Promise<HTMLElement[]> {
|
||||
return Array.from(
|
||||
document.querySelectorAll('[data-connection-button], [data-connection-button] *')
|
||||
) as HTMLElement[]
|
||||
}
|
||||
|
||||
function handleConnect() {
|
||||
if (isOpen) {
|
||||
deactivateConnection()
|
||||
return
|
||||
}
|
||||
activateConnection()
|
||||
}
|
||||
|
||||
function activateConnection() {
|
||||
selected = true
|
||||
openConnection()
|
||||
}
|
||||
|
||||
function deactivateConnection() {
|
||||
selected = false
|
||||
closeConnection()
|
||||
}
|
||||
|
||||
$: !isOpen && (selected = false)
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
use:clickOutside={{
|
||||
capture: true,
|
||||
stopPropagation: isOpen,
|
||||
exclude: getConnectionButtonElements
|
||||
}}
|
||||
use:pointerDownOutside={{
|
||||
capture: true,
|
||||
stopPropagation: isOpen,
|
||||
exclude: getConnectionButtonElements
|
||||
}}
|
||||
on:keydown|preventDefault|stopPropagation={(e) => e.key === 'Escape' && handleConnect()}
|
||||
on:pointerdown_outside={deactivateConnection}
|
||||
on:click_outside={deactivateConnection}
|
||||
class="connection-access"
|
||||
data-connection-button
|
||||
>
|
||||
<AnimatedButton
|
||||
animate={isOpen && selected}
|
||||
baseRadius="6px"
|
||||
animationDuration="2s"
|
||||
marginWidth="2px"
|
||||
>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="border"
|
||||
color="light"
|
||||
title="Connect"
|
||||
on:click={handleConnect}
|
||||
id="schema-plug"
|
||||
>
|
||||
<Plug size={14} />
|
||||
</Button>
|
||||
</AnimatedButton>
|
||||
</div>
|
||||
@@ -9,6 +9,7 @@
|
||||
import { Download, PanelRightOpen } from 'lucide-svelte'
|
||||
import S3FilePicker from '../S3FilePicker.svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import AnimatedButton from '$lib/components/common/button/AnimatedButton.svelte'
|
||||
|
||||
export let json: any
|
||||
export let level = 0
|
||||
@@ -21,8 +22,10 @@
|
||||
export let collapseLevel: number | undefined = undefined
|
||||
export let prefix = ''
|
||||
export let expandedEvenOnLevel0: string | undefined = undefined
|
||||
export let connecting = false
|
||||
|
||||
let s3FileViewer: S3FilePicker
|
||||
let hoveredKey: string | null = null
|
||||
|
||||
const collapsedSymbol = '...'
|
||||
$: keys = ['object', 's3object'].includes(getTypeAsString(json)) ? Object.keys(json) : []
|
||||
@@ -103,17 +106,29 @@
|
||||
>
|
||||
{#each keys.length > keyLimit ? keys.slice(0, keyLimit) : keys as key, index (key)}
|
||||
<li>
|
||||
<Button
|
||||
on:click={() => selectProp(key, undefined, false)}
|
||||
size="xs2"
|
||||
color="light"
|
||||
variant="border"
|
||||
wrapperClasses="inline-flex p-0 whitespace-nowrap w-fit"
|
||||
btnClasses="font-mono h-4 text-2xs font-thin px-1 rounded-[0.275rem]"
|
||||
title={computeFullKey(key, rawKey)}
|
||||
<AnimatedButton
|
||||
animate={connecting && hoveredKey === key}
|
||||
marginWidth="1px"
|
||||
wrapperClasses="inline-flex h-fit w-fit items-center"
|
||||
baseRadius="0.275rem"
|
||||
animationDuration="2s"
|
||||
>
|
||||
<span class={pureViewer ? 'cursor-auto' : ''}>{!isArray ? key : index} </span>
|
||||
</Button>
|
||||
<Button
|
||||
on:click={() => selectProp(key, undefined, false)}
|
||||
on:mouseenter={() => {
|
||||
hoveredKey = key
|
||||
}}
|
||||
on:mouseleave={() => (hoveredKey = null)}
|
||||
size="xs2"
|
||||
color="light"
|
||||
variant="border"
|
||||
wrapperClasses="p-0 whitespace-nowrap w-fit"
|
||||
btnClasses="font-mono h-4 py-1 text-2xs font-thin px-1 rounded-[0.275rem]"
|
||||
title={computeFullKey(key, rawKey)}
|
||||
>
|
||||
<span class={pureViewer ? 'cursor-auto' : ''}>{!isArray ? key : index} </span>
|
||||
</Button>
|
||||
</AnimatedButton>
|
||||
<span class="text-2xs -ml-0.5 text-tertiary">:</span>
|
||||
|
||||
{#if getTypeAsString(json[key]) === 'object'}
|
||||
|
||||
@@ -174,6 +174,7 @@ const portalDivs = ['app-editor-select']
|
||||
interface ClickOutsideOptions {
|
||||
capture?: boolean
|
||||
exclude?: (() => Promise<HTMLElement[]>) | HTMLElement[] | undefined
|
||||
stopPropagation?: boolean
|
||||
}
|
||||
|
||||
export function clickOutside(
|
||||
@@ -204,6 +205,9 @@ export function clickOutside(
|
||||
const parent = target.closest(portalDivsSelector)
|
||||
|
||||
if (!parent) {
|
||||
if (opts?.stopPropagation) {
|
||||
event.stopPropagation()
|
||||
}
|
||||
node.dispatchEvent(new CustomEvent<MouseEvent>('click_outside', { detail: event }))
|
||||
}
|
||||
}
|
||||
@@ -222,6 +226,56 @@ export function clickOutside(
|
||||
}
|
||||
}
|
||||
|
||||
export function pointerDownOutside(
|
||||
node: Node,
|
||||
options?: ClickOutsideOptions
|
||||
): { destroy(): void; update(newOptions: ClickOutsideOptions): void } {
|
||||
const handlePointerDown = async (event: PointerEvent) => {
|
||||
const target = event.target as HTMLElement
|
||||
|
||||
let excludedElements: HTMLElement[] = []
|
||||
if (options?.exclude) {
|
||||
if (Array.isArray(options.exclude)) {
|
||||
excludedElements = options.exclude
|
||||
} else {
|
||||
excludedElements = await options.exclude()
|
||||
}
|
||||
}
|
||||
|
||||
const isExcluded = excludedElements.some((excludedEl) => {
|
||||
const contains = excludedEl?.contains?.(target)
|
||||
const isTarget = target === excludedEl
|
||||
return contains || isTarget
|
||||
})
|
||||
|
||||
if (node && !node.contains(target) && !event.defaultPrevented && !isExcluded) {
|
||||
const portalDivsSelector = portalDivs.map((id) => `#${id}`).join(', ')
|
||||
const parent = target.closest(portalDivsSelector)
|
||||
|
||||
if (!parent) {
|
||||
if (options?.stopPropagation) {
|
||||
event.stopPropagation()
|
||||
}
|
||||
console.log('dbg pointerdown_outside')
|
||||
node.dispatchEvent(new CustomEvent<PointerEvent>('pointerdown_outside', { detail: event }))
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const capture = options?.capture ?? true
|
||||
document.addEventListener('pointerdown', handlePointerDown, capture ?? true)
|
||||
|
||||
return {
|
||||
update(newOptions: ClickOutsideOptions) {
|
||||
options = newOptions
|
||||
},
|
||||
destroy() {
|
||||
document.removeEventListener('pointerdown', handlePointerDown, capture ?? true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface DropdownItem {
|
||||
// If a DropdownItem has an action, it will be declared as a button
|
||||
// If a DropdownItem has no action and an href, it will be declared as a link
|
||||
|
||||
Reference in New Issue
Block a user