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:
Guilhem
2024-11-19 16:34:42 +01:00
committed by GitHub
parent 24343a6d4f
commit eac49d8942
17 changed files with 433 additions and 233 deletions
+1
View File
@@ -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' : '',
@@ -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>
@@ -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'}
+54
View File
@@ -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