fix(frontend): improve flow prop picker design

* improve input picker

* Put back colors for types

* Add full path to popover

* Copy instead of toast if no input selected

* Add popover and copy to value

* Add shadow en transition when selecting input

* Add border and transition when selecting input

* revert unwanted changes

* hide scrollbar when not hovering

* Add connexion animated border effect

* fix display

* fix proppicker display

* Remove badge

* Change animated button gradient

* revert scrollbar on hover

* clean design

* clean design

* clean design

* clean design

* clean design

* clean design

* clean design

* improve search

---------

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
This commit is contained in:
Guilhem
2024-11-05 21:01:49 +01:00
committed by Ruben Fiszel
co-authored by Ruben Fiszel
parent 63841a6950
commit 1d2e8c0eef
11 changed files with 547 additions and 382 deletions
@@ -9,9 +9,11 @@
import type { PropPickerWrapperContext } from './flows/propPicker/PropPickerWrapper.svelte'
import { codeToStaticTemplate, getDefaultExpr } from './flows/utils'
import SimpleEditor from './SimpleEditor.svelte'
import { Button } from './common'
import { Button } from '$lib/components/common'
import AnimatedButton from '$lib/components/common/button/AnimatedButton.svelte'
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
import { fade } from 'svelte/transition'
import type VariableEditor from './VariableEditor.svelte'
import type ItemPicker from './ItemPicker.svelte'
@@ -23,7 +25,7 @@
import type { FlowCopilotContext } from './copilot/flow'
import StepInputGen from './copilot/StepInputGen.svelte'
import type { PickableProperties } from './flows/previousResults'
import { twMerge } from 'tailwind-merge'
export let schema: Schema | { properties?: Record<string, any>; required?: string[] }
export let arg: InputTransform | any
export let argName: string
@@ -186,11 +188,22 @@
let stepInputGen: StepInputGen | undefined = undefined
loadResourceTypes()
$: connecting =
$propPickerConfig?.propName == argName && $propPickerConfig?.insertionMode == 'connect'
</script>
{#if arg != undefined}
<div class={$$props.class}>
<div class="flex flex-row justify-between gap-1 pb-1">
<div
class={twMerge(
'pl-2 pt-2 pb-2 ml-2 relative hover:bg-surface hover:shadow-md transition-all duration-200',
$propPickerConfig?.propName == argName
? 'bg-surface border-l-4 border-blue-500 shadow-md rounded-l-md z-2000'
: 'hover:rounded-md',
$$props.class
)}
>
<div class="flex flex-row justify-between gap-1 pb-1 px-2">
<div class="flex flex-wrap grow">
<FieldHeader
label={argName}
@@ -322,147 +335,172 @@
</ToggleButtonGroup>
</div>
<Button
title="Connect to another node's output"
variant="border"
color="light"
size="xs2"
on:click={() => {
focusProp(argName, 'connect', (path) => {
connectProperty(path)
dispatch('change', { argName })
return true
})
}}
id="flow-editor-plug"
<AnimatedButton
animate={connecting}
baseRadius="6px"
animationDuration="2s"
marginWidth="2px"
>
<Plug size={16} /> &rightarrow;
</Button>
<Button
variant="border"
color="light"
size="xs2"
btnClasses={connecting ? 'text-blue-500' : 'text-primary'}
on:click={() => {
focusProp(argName, 'connect', (path) => {
connectProperty(path)
dispatch('change', { argName })
return true
})
}}
>
<Plug size={16} /> &rightarrow;
</Button>
</AnimatedButton>
</div>
{/if}
</div>
<div class="max-w-xs" />
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="relative {$propPickerConfig?.propName == argName
? 'outline outline-offset-1 outline-1 outline-blue-500 rounded-md'
: ''}"
on:keyup={stepInputGen?.onKeyUp}
>
{#if $propPickerConfig?.propName == argName && $propPickerConfig?.insertionMode == 'connect'}
<div class="relative" on:keyup={stepInputGen?.onKeyUp}>
<!-- {#if $propPickerConfig?.propName == argName && $propPickerConfig?.insertionMode == 'connect'}
<span
class={'text-white z-50 px-1 text-2xs py-0.5 font-bold rounded-t-sm w-fit absolute top-0 right-0 bg-blue-500'}
>
Connect input &rightarrow;
</span>
{/if}
{/if} -->
<!-- {inputCat}
{propertyType} -->
{#if isStaticTemplate(inputCat) && propertyType == 'static' && !noDynamicToggle}
{#if argName && schema?.properties?.[argName]?.description}
<div class="text-xs italic pb-1 text-secondary">
<pre class="font-main">{schema.properties[argName].description}</pre>
</div>
{/if}
<div class="mt-2 min-h-[28px]">
{#if arg}
<TemplateEditor
bind:this={monacoTemplate}
{extraLib}
<div class="relative flex flex-row items-top gap-2 justify-between">
<div class="min-w-0 grow">
{#if isStaticTemplate(inputCat) && propertyType == 'static' && !noDynamicToggle}
{#if argName && schema?.properties?.[argName]?.description}
<div class="text-xs italic pb-1 text-secondary">
<pre class="font-main">{schema.properties[argName].description}</pre>
</div>
{/if}
<div class="mt-2 min-h-[28px]">
{#if arg}
<TemplateEditor
bind:this={monacoTemplate}
{extraLib}
on:focus={onFocus}
on:blur={() => {
focused = false
}}
bind:code={arg.value}
fontSize={14}
on:change={() => {
dispatch('change', { argName })
}}
/>
{/if}
</div>
{:else if (propertyType === undefined || propertyType == 'static') && schema?.properties?.[argName]}
<ArgInput
{resourceTypes}
noMargin
compact
bind:this={argInput}
on:focus={onFocus}
on:blur={() => {
focused = false
}}
bind:code={arg.value}
fontSize={14}
shouldDispatchChanges
on:change={() => {
dispatch('change', { argName })
}}
label={argName}
bind:editor={monaco}
bind:description={schema.properties[argName].description}
bind:value={arg.value}
type={schema.properties[argName].type}
oneOf={schema.properties[argName].oneOf}
required={schema.required?.includes(argName)}
bind:pattern={schema.properties[argName].pattern}
bind:valid={inputCheck}
defaultValue={schema.properties[argName].default}
bind:enum_={schema.properties[argName].enum}
bind:format={schema.properties[argName].format}
contentEncoding={schema.properties[argName].contentEncoding}
bind:itemsType={schema.properties[argName].items}
properties={schema.properties[argName].properties}
nestedRequired={schema.properties[argName].required}
displayHeader={false}
extra={argExtra}
{variableEditor}
{itemPicker}
bind:pickForField
showSchemaExplorer
nullable={schema.properties[argName].nullable}
bind:title={schema.properties[argName].title}
bind:placeholder={schema.properties[argName].placeholder}
/>
{:else if arg.expr != undefined}
<div class="border mt-2">
<SimpleEditor
bind:this={monaco}
bind:code={arg.expr}
on:change={() => {
dispatch('change', { argName })
}}
{extraLib}
lang="javascript"
shouldBindKey={false}
on:focus={() => {
focused = true
focusProp(argName, 'insert', (path) => {
monaco?.insertAtCursor(path)
return false
})
}}
on:change={() => {
dispatch('change', { argName })
}}
on:blur={() => {
focused = false
}}
autoHeight
/>
</div>
<DynamicInputHelpBox />
<div class="mb-2" />
{:else}
Not recognized input type {argName} ({arg.expr}, {propertyType})
<div class="flex mt-2">
<Button
variant="border"
size="xs"
on:click={() => {
arg.expr = ''
}}>Set expr to empty string</Button
></div
>
{/if}
</div>
{:else if (propertyType === undefined || propertyType == 'static') && schema?.properties?.[argName]}
<ArgInput
{resourceTypes}
noMargin
compact
bind:this={argInput}
on:focus={onFocus}
on:blur={() => {
focused = false
}}
shouldDispatchChanges
on:change={() => {
dispatch('change', { argName })
}}
label={argName}
bind:editor={monaco}
bind:description={schema.properties[argName].description}
bind:value={arg.value}
type={schema.properties[argName].type}
oneOf={schema.properties[argName].oneOf}
required={schema.required?.includes(argName)}
bind:pattern={schema.properties[argName].pattern}
bind:valid={inputCheck}
defaultValue={schema.properties[argName].default}
bind:enum_={schema.properties[argName].enum}
bind:format={schema.properties[argName].format}
contentEncoding={schema.properties[argName].contentEncoding}
bind:itemsType={schema.properties[argName].items}
properties={schema.properties[argName].properties}
nestedRequired={schema.properties[argName].required}
displayHeader={false}
extra={argExtra}
{variableEditor}
{itemPicker}
bind:pickForField
showSchemaExplorer
nullable={schema.properties[argName].nullable}
bind:title={schema.properties[argName].title}
bind:placeholder={schema.properties[argName].placeholder}
/>
{:else if arg.expr != undefined}
<div class="border mt-2">
<SimpleEditor
bind:this={monaco}
bind:code={arg.expr}
on:change={() => {
dispatch('change', { argName })
}}
{extraLib}
lang="javascript"
shouldBindKey={false}
on:focus={() => {
focused = true
focusProp(argName, 'insert', (path) => {
monaco?.insertAtCursor(path)
return false
})
}}
on:change={() => {
dispatch('change', { argName })
}}
on:blur={() => {
focused = false
}}
autoHeight
/>
</div>
<DynamicInputHelpBox />
<div class="mb-2" />
{:else}
Not recognized input type {argName} ({arg.expr}, {propertyType})
<div class="flex mt-2">
<Button
variant="border"
size="xs"
on:click={() => {
arg.expr = ''
}}>Set expr to empty string</Button
></div
>
{/if}
{#if $propPickerConfig?.propName == argName}
<div class="text-blue-500 mt-2" in:fade={{ duration: 200 }}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="24 24 12 12 24 0" />
</svg>
</div>
{:else}
<div class="w-0" />
{/if}
</div>
</div>
</div>
{/if}
@@ -67,24 +67,26 @@
<div class="w-full {clazz}">
{#if enableAi}
<StepInputsGen
{pickableProperties}
argNames={keys
? keys.filter(
(argName) =>
Object.keys(schema.properties ?? {}).includes(argName) &&
Object.keys(args ?? {}).includes(argName) &&
((args[argName].type === 'static' && !args[argName].value) ||
(args[argName].type === 'javascript' && !args[argName].expr))
)
: []}
{schema}
/>
<div class="px-0.5 pt-0.5">
<StepInputsGen
{pickableProperties}
argNames={keys
? keys.filter(
(argName) =>
Object.keys(schema.properties ?? {}).includes(argName) &&
Object.keys(args ?? {}).includes(argName) &&
((args[argName].type === 'static' && !args[argName].value) ||
(args[argName].type === 'javascript' && !args[argName].expr))
)
: []}
{schema}
/>
</div>
{/if}
{#if keys.length > 0}
{#each keys as argName (argName)}
{#if (!filter || filter.includes(argName)) && Object.keys(schema.properties ?? {}).includes(argName)}
<div class="z-10 pt-4">
<div class="z-10 pt-2 relative">
<InputTransformForm
{previousModuleId}
bind:arg={args[argName]}
@@ -128,7 +130,7 @@
>
<div
slot="submission"
class="flex flex-row-reverse w-full bg-surface border-t border-gray-200 rounded-bl-lg rounded-br-lg"
class="flex flex-row-reverse w-full border-t border-gray-200 rounded-bl-lg rounded-br-lg"
>
<Button
variant="border"
@@ -41,12 +41,14 @@
{#if object != undefined && Object.keys(object).length > 0}
{#if $hasResult[componentId] || $search == ''}
<ObjectViewer
json={filtered}
on:select
topBrackets={false}
pureViewer={!$connectingInput.opened}
/>
<div class="pl-2">
<ObjectViewer
json={filtered}
on:select
topBrackets={false}
pureViewer={!$connectingInput.opened}
/>
</div>
{:else if $search.length > 0}
<div class="text-xs pl-2 text-tertiary">No results</div>
{:else}
@@ -0,0 +1,93 @@
<script lang="ts">
import { twMerge } from 'tailwind-merge'
import DarkModeObserver from '$lib/components/DarkModeObserver.svelte'
export let marginWidth = '2px'
export let animationDuration = '2s'
export let baseRadius = '4px'
export let animate = true
export let wrapperClasses = ''
export let ringColor = 'transparent'
export let darkMode = false
const gradientColors = {
light: ['#d6e5ff', '#0073ff', '#5aa2fa', '#0272fa', '#d6e5ff'],
dark: ['#0469db', '#15498a', '#031ea3', '#0073ff', '#0469db']
}
let clientWidth = 0
let clientHeight = 0
$: circleRadius = Math.ceil(
Math.sqrt(clientWidth * clientWidth + clientHeight * clientHeight) / 2
)
$: gradientString = `from 0deg, ${gradientColors[darkMode ? 'dark' : 'light'].join(', ')}`
</script>
<DarkModeObserver bind:darkMode />
<div
class={twMerge('gradient-button', wrapperClasses)}
style="--margin-width: {marginWidth}; --animation-duration: {animationDuration}; --base-radius: {baseRadius}; --circle-radius: {circleRadius}; --ring-color: {ringColor}; --gradient: {gradientString}"
class:animate
bind:clientWidth
bind:clientHeight
>
<slot />
</div>
<style>
.gradient-button {
position: relative;
font-size: inherit;
border: none;
border-radius: calc(var(--base-radius) + var(--margin-width, 2px));
color: currentColor;
background: inherit;
z-index: 1;
overflow: hidden;
padding: var(--margin-width, 2px);
}
/* Circular gradient */
.gradient-button::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: calc(var(--circle-radius, 300px) * 2px);
height: calc(var(--circle-radius, 300px) * 2px);
background: var(--ring-color, transparent);
border-radius: 50%;
z-index: -1;
animation: none;
}
.gradient-button.animate::before {
background: conic-gradient(var(--gradient));
animation: rotate var(--animation-duration, 2s) linear infinite;
}
/* inner background */
.gradient-button::after {
content: '';
position: absolute;
top: var(--margin-width, 2px);
right: var(--margin-width, 2px);
bottom: var(--margin-width, 2px);
left: var(--margin-width, 2px);
background: inherit;
border-radius: var(--base-radius);
z-index: -1;
}
@keyframes rotate {
from {
transform: translate(-50%, -50%) rotate(0deg);
}
to {
transform: translate(-50%, -50%) rotate(360deg);
}
}
</style>
@@ -60,8 +60,8 @@
{#if value && isHovered}
<button
transition:fade|local={{ duration: 80 }}
class="absolute z-10 top-[9.5px] right-2 rounded-full p-0.5 text-white bg-gray-300
duration-200 hover:bg-gray-600 focus:bg-gray-600 {buttonClass}"
class="absolute z-10 top-[9.5px] right-2 rounded-full p-0.5 text-primary bg-surface-secondary
duration-200 hover:bg-surface-hovr focus:bg-surface-hover {buttonClass}"
aria-label="Clear"
on:click|preventDefault|stopPropagation={clear}
>
@@ -378,10 +378,11 @@
class={advancedSelected === 'runtime' ? 'h-[calc(100%-68px)]' : 'h-[calc(100%-34px)]'}
>
{#if selected === 'inputs' && (flowModule.value.type == 'rawscript' || flowModule.value.type == 'script' || flowModule.value.type == 'flow')}
<div class="h-full overflow-auto px-2" id="flow-editor-step-input">
<div class="h-full overflow-auto px-2 bg-surface" id="flow-editor-step-input">
<PropPickerWrapper
pickableProperties={stepPropPicker.pickableProperties}
error={failureModule}
noPadding
>
<InputTransformSchemaForm
bind:this={inputTransformSchemaForm}
@@ -19,12 +19,13 @@
<script lang="ts">
import PropPicker from '$lib/components/propertyPicker/PropPicker.svelte'
import PropPickerResult from '$lib/components/propertyPicker/PropPickerResult.svelte'
import { clickOutside, sendUserToast } from '$lib/utils'
import { clickOutside } from '$lib/utils'
import { createEventDispatcher, setContext } from 'svelte'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import { writable, type Writable } from 'svelte/store'
import type { PickableProperties } from '../previousResults'
import { twMerge } from 'tailwind-merge'
import AnimatedButton from '$lib/components/common/button/AnimatedButton.svelte'
export let pickableProperties: PickableProperties | undefined
export let result: any = undefined
@@ -58,51 +59,70 @@
use:clickOutside
on:click_outside={() => propPickerConfig.set(undefined)}
>
<Splitpanes>
<Splitpanes class={$propPickerConfig ? 'splitpanes-remove-splitter' : ''}>
<Pane
minSize={20}
size={60}
class={twMerge('relative !transition-none', noPadding ? '' : 'p-2')}
class={twMerge('relative !transition-none ', noPadding ? '' : 'p-2')}
>
<slot />
</Pane>
<Pane
minSize={20}
size={40}
class="pt-2 relative !transition-none {$propPickerConfig ? 'border-2 border-blue-500' : ''}"
class="!transition-none z-1000 {$propPickerConfig ? 'ml-[-1px]' : ''}"
>
{#if result}
<PropPickerResult
{result}
{extraResults}
{flow_input}
on:select={({ detail }) => {
if (!notSelectable && !$propPickerConfig) {
sendUserToast('Set cursor within an input or click on the plug first', true)
}
dispatch('select', detail)
if ($propPickerConfig?.onSelect(detail)) {
propPickerConfig.set(undefined)
}
}}
/>
{:else if pickableProperties}
<PropPicker
{displayContext}
{error}
{pickableProperties}
{notSelectable}
on:select={({ detail }) => {
if (!notSelectable && !$propPickerConfig) {
sendUserToast('Set cursor within an input or click on the plug first', true)
}
dispatch('select', detail)
if ($propPickerConfig?.onSelect(detail)) {
propPickerConfig.set(undefined)
}
}}
/>
{/if}
<AnimatedButton
animate={$propPickerConfig?.insertionMode == 'connect'}
baseRadius="4px"
wrapperClasses="h-full w-full pt-2 !bg-surface"
marginWidth="3px"
ringColor={$propPickerConfig?.insertionMode == 'insert' ||
$propPickerConfig?.insertionMode == 'append'
? '#3b82f6'
: 'transparent'}
animationDuration="4s"
>
{#if result}
<PropPickerResult
{result}
{extraResults}
{flow_input}
allowCopy={!notSelectable && !$propPickerConfig}
on:select={({ detail }) => {
dispatch('select', detail)
if ($propPickerConfig?.onSelect(detail)) {
propPickerConfig.set(undefined)
}
}}
/>
{:else if pickableProperties}
<PropPicker
{displayContext}
{error}
{pickableProperties}
{notSelectable}
allowCopy={!notSelectable && !$propPickerConfig}
on:select={({ detail }) => {
dispatch('select', detail)
if ($propPickerConfig?.onSelect(detail)) {
propPickerConfig.set(undefined)
}
}}
/>
{/if}
</AnimatedButton>
</Pane>
</Splitpanes>
</div>
<style>
:global(.splitpanes-remove-splitter > .splitpanes__pane) {
background-color: inherit !important;
}
:global(.splitpanes-remove-splitter > .splitpanes__splitter) {
background-color: transparent !important;
width: 0 !important;
border: none !important;
}
</style>
@@ -1,13 +1,11 @@
<script lang="ts">
import { copyToClipboard, pluralize, truncate } from '$lib/utils'
import { copyToClipboard, truncate } from '$lib/utils'
import { createEventDispatcher } from 'svelte'
import { Badge } from '../common'
import { computeKey } from './utils'
import WarningMessage from './WarningMessage.svelte'
import { NEVER_TESTED_THIS_FAR } from '../flows/models'
import Portal from '$lib/components/Portal.svelte'
import { Button } from '$lib/components/common'
import { Download, PanelRightOpen } from 'lucide-svelte'
import S3FilePicker from '../S3FilePicker.svelte'
import { workspaceStore } from '$lib/stores'
@@ -19,9 +17,9 @@
export let collapsed = (level != 0 && level % 3 == 0) || Array.isArray(json)
export let rawKey = false
export let topBrackets = false
export let topLevelNode = false
export let allowCopy = true
export let collapseLevel: number | undefined = undefined
export let prefix = ''
let s3FileViewer: S3FilePicker
@@ -50,12 +48,21 @@
const dispatch = createEventDispatcher()
function selectProp(key: string, value: any | undefined = undefined) {
if (pureViewer && allowCopy) {
const valueToCopy = value !== undefined ? value : computeKey(key, isArray, currentPath)
copyToClipboard(valueToCopy)
function computeFullKey(key: string, rawKey: boolean) {
if (rawKey) {
return `${prefix}('${key}')`
}
dispatch('select', rawKey ? key : computeKey(key, isArray, currentPath))
const keyToSelect = computeKey(key, isArray, currentPath)
const separator = !prefix || keyToSelect.startsWith('[') ? '' : '.'
return prefix + separator + keyToSelect
}
function selectProp(key: string, value: any | undefined = undefined) {
const fullKey = computeFullKey(key, rawKey)
if (pureViewer && allowCopy) {
copyToClipboard(fullKey)
}
dispatch('select', fullKey)
}
$: keyLimit = isArray ? 1 : 100
@@ -66,40 +73,44 @@
<Portal name="object-viewer">
<S3FilePicker bind:this={s3FileViewer} readOnlyMode={true} />
</Portal>
{#if keys.length > 0}
{#if !fullyCollapsed}
<span>
{#if level != 0 && keys.length > 1}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span class="cursor-pointer border hover:bg-surface-hover px-1 rounded" on:click={collapse}>
-
</span>
<Button
color="light"
size="xs2"
variant="border"
on:click={collapse}
wrapperClasses="inline-flex w-fit h-4"
btnClasses="font-semibold text-primary rounded-[0.275rem]">-</Button
>
{/if}
{#if level == 0 && topBrackets}<span class="h-0">{openBracket}</span>{/if}
<ul class={`w-full pl-2 ${level === 0 ? 'border-none' : 'border-l border-dotted'}`}>
<ul class={`w-full ${level === 0 ? 'border-none' : 'pl-2 border-l border-dotted'}`}>
{#each keys.length > keyLimit ? keys.slice(0, keyLimit) : keys as key, index (key)}
<li>
<button on:click={() => selectProp(key)} class="whitespace-nowrap">
{#if topLevelNode}
<Badge baseClass="border border-blue-600" color="indigo">{key}</Badge>
{:else}
<span
class="key {pureViewer
? 'cursor-auto'
: 'border '} font-semibold rounded px-1 hover:bg-surface-hover text-2xs text-secondary"
>
{!isArray ? key : index}</span
>
{/if}:
</button>
<div class="inline-flex items-baseline">
<Button
on:click={() => selectProp(key)}
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)}
>
<span class={pureViewer ? 'cursor-auto' : ''}>{!isArray ? key : index} </span>
</Button>
<span class="text-2xs text-tertiary">:</span>
</div>
{#if getTypeAsString(json[key]) === 'object'}
<svelte:self
json={json[key]}
level={level + 1}
currentPath={computeKey(key, isArray, currentPath)}
currentPath={computeFullKey(key, rawKey)}
{pureViewer}
{allowCopy}
on:select
@@ -110,21 +121,25 @@
<button
class="val text-left {pureViewer
? 'cursor-auto'
: ''} rounded px-1 hover:bg-blue-100 dark:hover:bg-blue-100/10 {getTypeAsString(
json[key]
)}"
on:click={() => selectProp(key, json[key])}
: ''} rounded px-1 {getTypeAsString(json[key])}"
on:click={() => {
selectProp(key, json[key])
}}
title={computeFullKey(key, rawKey)}
disabled={false}
>
{#if json[key] === NEVER_TESTED_THIS_FAR}
<WarningMessage />
<span class="text-2xs text-tertiary font-normal">
Test the flow to see a value
</span>
{:else if json[key] == undefined}
<span class="text-2xs">undefined</span>
{:else if json[key] == null}
<span class="text-2xs">null</span>
{:else if typeof json[key] == 'string'}
<span title={json[key]} class="text-2xs">"{truncate(json[key], 200)}"</span>
<span class="text-2xs">"{truncate(json[key], 200)}"</span>
{:else}
<span title={JSON.stringify(json[key])} class="text-2xs">
<span class="text-2xs">
{truncate(JSON.stringify(json[key]), 200)}
</span>
{/if}
@@ -134,7 +149,7 @@
{/each}
{#if keys.length > keyLimit}
{@const increment = Math.min(100, keys.length - keyLimit)}
<button on:click={() => (keyLimit += increment)} class="text-xs py-2 text-blue-600">
<button on:click={() => (keyLimit += increment)} class="text-2xs px-2 text-secondary">
{keyLimit}/{keys.length}: Load {increment} more...
</button>
{/if}
@@ -167,24 +182,27 @@
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span
class="border border-blue-600 rounded px-1 cursor-pointer hover:bg-gray-200"
class:hidden={!fullyCollapsed}
on:click={collapse}
>
{openBracket}{collapsedSymbol}{closeBracket}
</span>
{#if fullyCollapsed}
<span class="text-tertiary text-xs">
{pluralize(Object.keys(json).length, Array.isArray(json) ? 'item' : 'key')}
</span>
<div class="inline-flex relative">
<Button
color="light"
size="xs2"
variant="border"
on:click={collapse}
wrapperClasses="inline-flex w-fit h-4"
btnClasses="font-normal rounded-[0.275rem] h-4 p-1"
>
{openBracket}{collapsedSymbol}{closeBracket}
</Button>
</div>
{/if}
{:else if topBrackets}
<span class="text-primary">{openBracket}{closeBracket}</span>
{:else if json == undefined}
<span class="text-tertiary text-2xs ml-2">undefined</span>
{:else}
<span class="text-tertiary text-2xs ml-2">No items ([])</span>
<span class="text-tertiary text-2xs ml-2">No items</span>
{/if}
<style lang="postcss">
@@ -2,9 +2,8 @@
import { ResourceService, VariableService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { getContext } from 'svelte'
import { Badge, Button } from '../common'
import { Button } from '../common'
import type { PropPickerWrapperContext } from '../flows/propPicker/PropPickerWrapper.svelte'
import { createEventDispatcher } from 'svelte'
import ObjectViewer from './ObjectViewer.svelte'
import { keepByKey } from './utils'
@@ -15,33 +14,45 @@
export let displayContext = true
export let notSelectable: boolean
export let error: boolean = false
export let allowCopy = false
$: previousId = pickableProperties?.previousId
let variables: Record<string, string> = {}
let resources: Record<string, any> = {}
let displayVariable = false
let displayResources = false
const dispatch = createEventDispatcher()
let allResultsCollapsed = true
const EMPTY_STRING = ''
let search = ''
const { propPickerConfig } = getContext<PropPickerWrapperContext>('PropPickerWrapper')
$: flowInputsFiltered =
search === EMPTY_STRING
? pickableProperties.flow_input
: keepByKey(pickableProperties.flow_input, search)
let flowInputsFiltered = pickableProperties.flow_input
let resultByIdFiltered = pickableProperties.priorIds
$: resultByIdFiltered =
search === EMPTY_STRING
? pickableProperties.priorIds
: keepByKey(pickableProperties.priorIds, search)
let timeout: NodeJS.Timeout
function onSearch(search: string) {
clearTimeout(timeout)
setTimeout(() => {
flowInputsFiltered =
search === EMPTY_STRING
? pickableProperties.flow_input
: keepByKey(pickableProperties.flow_input, search)
resultByIdFiltered =
search === EMPTY_STRING
? pickableProperties.priorIds
: keepByKey(pickableProperties.priorIds, search)
console.log(resultByIdFiltered, search)
}, 50)
}
$: suggestedPropsFiltered = $propPickerConfig
? keepByKey(pickableProperties.priorIds, $propPickerConfig.propName)
: undefined
$: search != undefined && onSearch(search)
async function loadVariables() {
variables = Object.fromEntries(
@@ -64,50 +75,29 @@
}
</script>
<div class="flex flex-col h-full">
<div class="px-2">
{#if !notSelectable}
<div class="flex flex-row space-x-1">
{#if $propPickerConfig}
<Badge large color="blue">
{`Selected: ${$propPickerConfig?.propName}`}
</Badge>
<Badge large color="blue">
{`Mode: ${$propPickerConfig?.insertionMode}`}
</Badge>
{:else}
<Badge large color="blue">&leftarrow; Edit or connect an input</Badge>
{/if}
</div>
{/if}
<ClearableInput bind:value={search} placeholder="Search prop..." wrapperClass="py-2" />
<div class="flex flex-col h-full rounded overflow-hidden">
<div class="px-2 py-2">
<ClearableInput bind:value={search} placeholder="Search prop..." />
</div>
<div
class="overflow-y-auto px-2 pt-2 grow"
class:bg-surface-secondary={!$propPickerConfig && !notSelectable}
>
<div class="overflow-y-auto px-2 pt-2 grow">
<div class="flex justify-between items-center space-x-1">
<span class="font-bold text-sm">Flow Input</span>
<span class="font-normal text-sm text-secondary">Flow Input</span>
<div class="flex space-x-2 items-center" />
</div>
<div class="overflow-y-auto mb-2">
<ObjectViewer
allowCopy={false}
{allowCopy}
pureViewer={!$propPickerConfig}
json={flowInputsFiltered}
on:select={(e) => {
dispatch(
'select',
e.detail?.startsWith('[') ? `flow_input${e.detail}` : `flow_input.${e.detail}`
)
}}
prefix="flow_input"
on:select
/>
</div>
{#if error}
<span class="font-bold text-sm">Error</span>
<span class="font-normal text-sm text-secondary">Error</span>
<div class="overflow-y-auto mb-2">
<ObjectViewer
allowCopy={false}
{allowCopy}
pureViewer={!$propPickerConfig}
json={{
error: {
@@ -122,158 +112,178 @@
</div>
{#if Object.keys(pickableProperties.priorIds).length > 0}
{#if suggestedPropsFiltered && Object.keys(suggestedPropsFiltered).length > 0}
<span class="font-bold text-sm">Suggested Results</span>
<span class="font-normal text-sm text-secondary">Suggested Results</span>
<div class="overflow-y-auto mb-2">
<ObjectViewer
allowCopy={false}
topLevelNode
{allowCopy}
pureViewer={!$propPickerConfig}
collapsed={false}
json={suggestedPropsFiltered}
on:select={(e) => {
dispatch('select', `results.${e.detail}`)
}}
prefix="results"
on:select
/>
</div>
{/if}
<span class="font-bold text-sm">All Results</span>
<span class="font-normal text-sm text-secondary">All Results</span>
<div class="overflow-y-auto mb-2">
<ObjectViewer
allowCopy={false}
topLevelNode
{allowCopy}
pureViewer={!$propPickerConfig}
collapsed={true}
json={resultByIdFiltered}
on:select={(e) => {
dispatch('select', `results.${e.detail}`)
}}
prefix="results"
on:select
/>
</div>
{/if}
{:else}
{#if previousId}
<span class="font-bold text-sm">Previous Result</span>
<span class="font-normal text-sm text-secondary">Previous Result</span>
<div class="overflow-y-auto mb-2">
<ObjectViewer
allowCopy={false}
topLevelNode
{allowCopy}
pureViewer={!$propPickerConfig}
json={Object.fromEntries(
Object.entries(resultByIdFiltered).filter(([k, v]) => k == previousId)
)}
on:select={(e) => {
dispatch('select', `results.${e.detail}`)
}}
prefix="results"
on:select
/>
</div>
{/if}
{#if pickableProperties.hasResume}
<span class="font-bold text-sm">Resume payloads</span>
<span class="font-normal text-sm text-secondary">Resume payloads</span>
<div class="overflow-y-auto mb-2">
<ObjectViewer
allowCopy={false}
topLevelNode
{allowCopy}
pureViewer={!$propPickerConfig}
json={{
resume: 'The resume payload',
resumes: 'All resume payloads from all approvers',
approvers: 'The list of approvers'
}}
on:select={(e) => {
dispatch('select', `${e.detail}`)
}}
on:select
/>
</div>
{/if}
{#if Object.keys(pickableProperties.priorIds).length > 0}
{#if suggestedPropsFiltered && Object.keys(suggestedPropsFiltered).length > 0}
<span class="font-bold text-sm">Suggested Results</span>
<span class="font-normal text-sm text-secondary">Suggested Results</span>
<div class="overflow-y-auto mb-2">
<ObjectViewer
allowCopy={false}
topLevelNode
{allowCopy}
pureViewer={!$propPickerConfig}
collapsed={false}
json={suggestedPropsFiltered}
on:select={(e) => {
dispatch('select', `results.${e.detail}`)
}}
prefix="results"
on:select
/>
</div>
{/if}
<span class="font-bold text-sm">All Results</span>
<div class="overflow-y-auto mb-2">
<span class="font-normal text-sm text-secondary">All Results</span>
{#if !allResultsCollapsed}
<Button
color="light"
size="xs2"
variant="contained"
on:click={() => {
allResultsCollapsed = true
}}
wrapperClasses="inline-flex w-fit h-4"
btnClasses="font-normal text-primary rounded-[0.275rem]">-</Button
>
{/if}
<ObjectViewer
allowCopy={false}
topLevelNode
{allowCopy}
pureViewer={!$propPickerConfig}
collapsed={true}
bind:collapsed={allResultsCollapsed}
json={resultByIdFiltered}
on:select={(e) => {
dispatch('select', `results.${e.detail}`)
}}
prefix="results"
on:select
/>
</div>
{/if}
{/if}
{#if displayContext}
<span class="font-bold text-sm">Variables </span>
<div class="overflow-y-auto mb-2">
<span class="font-normal text-sm text-secondary">Variables:</span>
{#if displayVariable}
<div class="flex">
<Button
color="light"
size="xs"
variant="border"
on:click={() => {
displayVariable = false
}}>-</Button
>
</div>
<Button
color="light"
size="xs2"
variant="border"
on:click={() => {
displayVariable = false
}}
wrapperClasses="inline-flex w-fit h-4"
btnClasses="font-normal text-primary rounded-[0.275rem]">-</Button
>
<ObjectViewer
allowCopy={false}
{allowCopy}
pureViewer={!$propPickerConfig}
rawKey={true}
json={variables}
on:select={(e) => dispatch('select', `variable('${e.detail}')`)}
prefix="variable"
on:select
/>
{:else}
<button
class="border border-blue-600 key font-normal rounded hover:bg-blue-100 px-1"
<Button
color="light"
size="xs2"
variant="border"
on:click={async () => {
await loadVariables()
displayVariable = true
}}>{'{...}'}</button
}}
wrapperClasses="inline-flex w-fit h-4"
btnClasses="font-normal rounded-[0.275rem] p-1"
>
{'{...}'}
</Button>
{/if}
</div>
<span class="font-bold text-sm">Resources</span>
<div class="overflow-y-auto mb-2">
<span class="font-normal text-sm text-secondary">Resources:</span>
{#if displayResources}
<Button
color="light"
size="xs2"
variant="border"
size="xs"
on:click={() => {
displayResources = false
}}>-</Button
}}
wrapperClasses="inline-flex w-fit h-4"
btnClasses="font-normal text-primary rounded-[0.275rem]">-</Button
>
<ObjectViewer
allowCopy={false}
{allowCopy}
pureViewer={!$propPickerConfig}
rawKey={true}
json={resources}
on:select={(e) => dispatch('select', `resource('${e.detail}')`)}
prefix="resource"
on:select
/>
{:else}
<button
class="border border-blue-600 px-1 key font-normal rounded hover:bg-blue-100"
<Button
color="light"
size="xs2"
variant="border"
on:click={async () => {
await loadResources()
displayResources = true
}}>{'{...}'}</button
}}
wrapperClasses="inline-flex w-fit h-4"
btnClasses="font-normal rounded-[0.275rem] p-1"
>
{'{...}'}
</Button>
{/if}
</div>
{/if}
@@ -1,31 +1,21 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import ObjectViewer from './ObjectViewer.svelte'
export let allowCopy = false
export let result: any
export let extraResults: any = undefined
export let flow_input: any = undefined
const dispatch = createEventDispatcher()
</script>
<div class="w-full px-2">
<span class="font-bold text-sm">Result</span>
<span class="font-normal text-sm text-secondary">Result</span>
<div class="overflow-y-auto mb-2 w-full">
<ObjectViewer
allowCopy={false}
json={{ result, ...(extraResults ? extraResults : {}) }}
on:select
/>
<ObjectViewer {allowCopy} json={{ result, ...(extraResults ? extraResults : {}) }} on:select />
</div>
{#if flow_input}
<span class="font-bold text-sm">Flow Input</span>
<span class="font-normal text-sm text-secondary">Flow Input</span>
<div class="overflow-y-auto w-full">
<ObjectViewer
allowCopy={false}
json={flow_input}
on:select={(e) => dispatch('select', `flow_input.${e.detail}`)}
/>
<ObjectViewer {allowCopy} json={flow_input} prefix="flow_input" on:select />
</div>
{/if}
</div>
@@ -1,44 +1,35 @@
function filterByKey(obj: Object, key: string): Object {
if (Object(obj) !== obj) {
return obj
function filterByKey(obj: Object, key: string): any {
if (typeof obj !== 'object' || obj === null) {
return undefined
} else if (Array.isArray(obj)) {
return obj.map((o) => filterByKey(o, key))
} else {
return Object.fromEntries(
Object.entries(obj)
.filter(([k, v]) => !k.includes(key))
.map(([k, v]) => [k, filterByKey(v, key)])
)
}
}
function diff(target: Object, source: Object): Object {
if (Array.isArray(target)) {
return target
}
const result = {}
Object.keys(target ?? {}).forEach((key: string) => {
if (typeof source[key] === 'object') {
const difference = diff(target[key], source[key])
if (Object.keys(difference).length > 0) {
result[key] = difference
}
} else if (source[key] !== target[key]) {
result[key] = target[key]
let a = obj
.map((k, o) =>
typeof o == 'object' ? filterByKey(o, key) : String(k - 1).includes(key) ? o : undefined
)
.filter((v) => v !== undefined)
if (a.length === 0) {
return undefined
} else {
return a
}
})
return result
} else {
let o = Object.fromEntries(
Object.entries(obj)
.map(([k, v]) => (k.includes(key) ? [k, v] : [k, filterByKey(v, key)]))
.filter(([k, v]) => v !== undefined)
)
if (Object.keys(o).length === 0) {
return undefined
}
return o
}
}
export function keepByKey(json: Object | undefined, key: string): Object {
if (!json) {
return {}
}
return diff(json, filterByKey(json, key))
return filterByKey(json, key) ?? {}
}
// https://stackoverflow.com/questions/23377217/way-to-test-if-a-string-is-valid-identifier-name-in-javascript