Compare commits

...

32 Commits

Author SHA1 Message Date
Guilhem
5d60cf36f7 Harmonize prop picker display 2024-11-05 10:13:14 +01:00
Guilhem
a196851998 hide suggested results when filtering input 2024-11-05 10:13:14 +01:00
Guilhem
08f82632c8 Move node prop picker 2024-11-05 10:13:14 +01:00
Guilhem
ff3e734a0d fix wrong code detection 2024-11-05 10:13:14 +01:00
Guilhem
5617a1b19b revert flow output view only 2024-11-05 10:13:14 +01:00
Guilhem
72102b7b1c Add auto filtering in prop-picker 2024-11-05 10:13:14 +01:00
Guilhem
a56e37564b change gradient for animated button 2024-11-05 10:13:14 +01:00
Guilhem
96af7c4349 fix minor issues 2024-11-05 10:13:14 +01:00
Guilhem
a6592cf34e show module output on hover 2024-11-05 10:13:14 +01:00
Guilhem
7b7e74b2ae remove unsused log 2024-11-05 10:13:14 +01:00
Guilhem
f5a4d25fc4 show node's output when selected 2024-11-05 10:13:14 +01:00
Guilhem
91416fe96d Add default view only connection button 2024-11-05 10:13:14 +01:00
Ruben Fiszel
8feb0bbc93 Merge branch 'main' into glm/improve-input-connection 2024-11-02 19:47:36 +01:00
Guilhem
c48946b2b3 Add javascript expression check in static input 2024-10-30 19:10:01 +01:00
Guilhem
d0218631ae fix layout issue 2024-10-30 16:53:15 +01:00
Guilhem
a6a91a413e neat 2024-10-30 15:57:16 +01:00
Guilhem
2f137094d8 Add result picker in flow 2024-10-30 14:43:45 +01:00
Guilhem
cc65c839f2 Add flow Module Prop picker 2024-10-30 10:25:45 +01:00
Guilhem
05a9cdd588 Add clickable prop in flow 2024-10-30 01:13:50 +01:00
Guilhem
f3738a3850 use prop picker context as a shared context 2024-10-29 22:01:47 +01:00
Guilhem
60878f24d8 Revert "Add a shared propPicker context"
This reverts commit c3f1569238.
2024-10-29 21:47:44 +01:00
Guilhem
c3f1569238 Add a shared propPicker context 2024-10-29 21:42:22 +01:00
Guilhem
f3d8348bb1 Add connexion animated border effect 2024-10-29 14:32:22 +01:00
Guilhem
7f50d92d34 hide scrollbar when not hovering 2024-10-29 08:46:16 +01:00
Guilhem
7779079fca revert unwanted changes 2024-10-29 08:45:04 +01:00
Guilhem
9984efafcc Add border and transition when selecting input 2024-10-28 18:00:28 +01:00
Guilhem
e4fc89cb99 Add shadow en transition when selecting input 2024-10-28 15:40:16 +01:00
Guilhem
aa48107e27 Add popover and copy to value 2024-10-28 11:24:47 +01:00
Guilhem
05b55f53c9 Copy instead of toast if no input selected 2024-10-28 10:50:48 +01:00
Guilhem
50cb4df130 Add full path to popover 2024-10-28 10:42:01 +01:00
Guilhem
a89dc8f845 Put back colors for types 2024-10-28 10:41:41 +01:00
Guilhem
63abd70f2e improve input picker 2024-10-23 19:09:50 +02:00
20 changed files with 1003 additions and 441 deletions

View File

@@ -6,13 +6,15 @@
import ArgInput from './ArgInput.svelte' import ArgInput from './ArgInput.svelte'
import FieldHeader from './FieldHeader.svelte' import FieldHeader from './FieldHeader.svelte'
import DynamicInputHelpBox from './flows/content/DynamicInputHelpBox.svelte' import DynamicInputHelpBox from './flows/content/DynamicInputHelpBox.svelte'
import type { PropPickerWrapperContext } from './flows/propPicker/PropPickerWrapper.svelte' import type { PropPickerWrapperContext } from './prop_picker'
import { codeToStaticTemplate, getDefaultExpr } from './flows/utils' import { codeToStaticTemplate, getDefaultExpr } from './flows/utils'
import SimpleEditor from './SimpleEditor.svelte' 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 ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte' import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
import { fade } from 'svelte/transition'
import { tick } from 'svelte'
import type VariableEditor from './VariableEditor.svelte' import type VariableEditor from './VariableEditor.svelte'
import type ItemPicker from './ItemPicker.svelte' import type ItemPicker from './ItemPicker.svelte'
import type { InputTransform } from '$lib/gen' import type { InputTransform } from '$lib/gen'
@@ -23,7 +25,8 @@
import type { FlowCopilotContext } from './copilot/flow' import type { FlowCopilotContext } from './copilot/flow'
import StepInputGen from './copilot/StepInputGen.svelte' import StepInputGen from './copilot/StepInputGen.svelte'
import type { PickableProperties } from './flows/previousResults' import type { PickableProperties } from './flows/previousResults'
import { buildPrefixRegex } from './flows/previousResults'
import { twMerge } from 'tailwind-merge'
export let schema: Schema | { properties?: Record<string, any>; required?: string[] } export let schema: Schema | { properties?: Record<string, any>; required?: string[] }
export let arg: InputTransform | any export let arg: InputTransform | any
export let argName: string export let argName: string
@@ -57,6 +60,8 @@
const { shouldUpdatePropertyType, exprsToSet } = const { shouldUpdatePropertyType, exprsToSet } =
getContext<FlowCopilotContext | undefined>('FlowCopilotContext') || {} getContext<FlowCopilotContext | undefined>('FlowCopilotContext') || {}
const { inputMatches } = getContext<PropPickerWrapperContext>('PropPickerWrapper')
function setExpr() { function setExpr() {
const newArg = $exprsToSet?.[argName] const newArg = $exprsToSet?.[argName]
if (newArg) { if (newArg) {
@@ -131,6 +136,54 @@
} }
} }
let codeInjectionDetected = false
const dynamicTemplateRegexPairs = buildPrefixRegex([
'flow_input',
'results',
'resource',
'variable'
])
function checkCodeInjection(rawValue: string) {
if (!arg || !rawValue || rawValue.length < 3 || !dynamicTemplateRegexPairs) {
return undefined
}
const matches = dynamicTemplateRegexPairs.filter(({ regex }) => regex.test(rawValue))
if (matches.length > 0) {
return matches.map((m) => ({ word: m.word, value: rawValue }))
}
return undefined
}
async function setJavaScriptExpr(rawValue: string) {
arg = {
type: 'javascript',
expr: rawValue
}
propertyType = 'javascript'
monaco?.setCode('')
monaco?.insertAtCursor(rawValue)
await tick()
monaco?.focus()
await tick()
monaco?.setCursorToEnd()
}
function handleKeyUp(e: KeyboardEvent) {
if (
e.key === 'Tab' &&
isStaticTemplate(inputCat) &&
propertyType == 'static' &&
!noDynamicToggle &&
codeInjectionDetected
) {
setJavaScriptExpr(arg.value)
} else {
stepInputGen?.onKeyUp?.(e)
}
}
function isStaticTemplate(inputCat: InputCat) { function isStaticTemplate(inputCat: InputCat) {
return inputCat === 'string' || inputCat === 'sql' || inputCat == 'yaml' return inputCat === 'string' || inputCat === 'sql' || inputCat == 'yaml'
} }
@@ -166,7 +219,24 @@
const { focusProp, propPickerConfig } = getContext<PropPickerWrapperContext>('PropPickerWrapper') const { focusProp, propPickerConfig } = getContext<PropPickerWrapperContext>('PropPickerWrapper')
$: isStaticTemplate(inputCat) && propertyType == 'static' && setPropertyType(arg?.value) $: updateStaticInput(inputCat, propertyType, arg)
function updateStaticInput(
inputCat: InputCat,
propertyType: 'static' | 'javascript',
arg: InputTransform | any
) {
if (!isStaticTemplate(inputCat)) {
return
}
if (propertyType == 'static') {
setPropertyType(arg?.value)
codeInjectionDetected = !!checkCodeInjection(arg?.value)
} else if (propertyType == 'javascript' && focused) {
setPropertyType(arg?.expr)
$inputMatches = checkCodeInjection(arg?.expr)
}
}
function setDefaultCode() { function setDefaultCode() {
if (!arg?.value) { if (!arg?.value) {
@@ -186,11 +256,22 @@
let stepInputGen: StepInputGen | undefined = undefined let stepInputGen: StepInputGen | undefined = undefined
loadResourceTypes() loadResourceTypes()
$: connecting =
$propPickerConfig?.propName == argName && $propPickerConfig?.insertionMode == 'connect'
</script> </script>
{#if arg != undefined} {#if arg != undefined}
<div class={$$props.class}> <div
<div class="flex flex-row justify-between gap-1 pb-1"> 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"> <div class="flex flex-wrap grow">
<FieldHeader <FieldHeader
label={argName} label={argName}
@@ -322,147 +403,181 @@
</ToggleButtonGroup> </ToggleButtonGroup>
</div> </div>
<Button <AnimatedButton animate={connecting} baseRadius="6px" animationDuration="1s">
title="Connect to another node's output" <Button
variant="border" variant="border"
color="light" color="light"
size="xs2" size="xs2"
on:click={() => { btnClasses={connecting ? 'text-blue-500' : 'text-primary'}
focusProp(argName, 'connect', (path) => { on:click={() => {
connectProperty(path) focusProp(argName, 'connect', (path) => {
dispatch('change', { argName }) connectProperty(path)
return true dispatch('change', { argName })
}) return true
}} })
id="flow-editor-plug" }}
> >
<Plug size={16} /> &rightarrow; <Plug size={16} /> &rightarrow;
</Button> </Button>
</AnimatedButton>
</div> </div>
{/if} {/if}
</div> </div>
<div class="max-w-xs" /> <div class="max-w-xs" />
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div <div class="relative" on:keyup={handleKeyUp}>
class="relative {$propPickerConfig?.propName == argName <!-- {#if $propPickerConfig?.propName == argName && $propPickerConfig?.insertionMode == 'connect'}
? 'outline outline-offset-1 outline-1 outline-blue-500 rounded-md'
: ''}"
on:keyup={stepInputGen?.onKeyUp}
>
{#if $propPickerConfig?.propName == argName && $propPickerConfig?.insertionMode == 'connect'}
<span <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'} 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; Connect input &rightarrow;
</span> </span>
{/if} {/if} -->
<!-- {inputCat} <!-- {inputCat}
{propertyType} --> {propertyType} -->
{#if isStaticTemplate(inputCat) && propertyType == 'static' && !noDynamicToggle} <div class="relative flex flex-row items-top gap-2 justify-between">
{#if argName && schema?.properties?.[argName]?.description} <div class="min-w-0 grow">
<div class="text-xs italic pb-1 text-secondary"> {#if isStaticTemplate(inputCat) && propertyType == 'static' && !noDynamicToggle}
<pre class="font-main">{schema.properties[argName].description}</pre> {#if argName && schema?.properties?.[argName]?.description}
</div> <div class="text-xs italic pb-1 text-secondary">
{/if} <pre class="font-main">{schema.properties[argName].description}</pre>
<div class="mt-2 min-h-[28px]"> </div>
{#if arg} {/if}
<TemplateEditor <div class="mt-2 min-h-[28px]">
bind:this={monacoTemplate} {#if arg}
{extraLib} <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>
{#if codeInjectionDetected}
<Button
size="xs"
color="light"
btnClasses="font-normal text-xs w-fit bg-green-100 text-green-800 hover:bg-green-100 dark:text-green-400 dark:bg-green-700 dark:hover:bg-green-700"
on:click={() => setJavaScriptExpr(arg.value)}
>
<span class="font-normal"
>JavaScript expression detected - press
<span class="font-bold">TAB</span> to exit static mode
</span>
</Button>
{/if}
{:else if (propertyType === undefined || propertyType == 'static') && schema?.properties?.[argName]}
<ArgInput
{resourceTypes}
noMargin
compact
bind:this={argInput}
on:focus={onFocus} on:focus={onFocus}
on:blur={() => { on:blur={() => {
focused = false focused = false
}} }}
bind:code={arg.value} shouldDispatchChanges
fontSize={14}
on:change={() => { on:change={() => {
dispatch('change', { argName }) 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}
</div> </div>
{:else if (propertyType === undefined || propertyType == 'static') && schema?.properties?.[argName]}
<ArgInput {#if $propPickerConfig?.propName == argName && ($propPickerConfig?.insertionMode == 'insert' || $propPickerConfig?.insertionMode == 'append')}
{resourceTypes} <div class="text-blue-500 mt-2" in:fade={{ duration: 200 }}>
noMargin <svg
compact xmlns="http://www.w3.org/2000/svg"
bind:this={argInput} width="20"
on:focus={onFocus} height="20"
on:blur={() => { viewBox="0 0 24 24"
focused = false fill="currentColor"
}} stroke="currentColor"
shouldDispatchChanges stroke-width="2"
on:change={() => { stroke-linecap="round"
dispatch('change', { argName }) stroke-linejoin="round"
}} >
label={argName} <polyline points="24 24 12 12 24 0" />
bind:editor={monaco} </svg>
bind:description={schema.properties[argName].description} </div>
bind:value={arg.value} {:else}
type={schema.properties[argName].type} <div class="w-0" />
oneOf={schema.properties[argName].oneOf} {/if}
required={schema.required?.includes(argName)} </div>
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> </div>
</div> </div>
{/if} {/if}

View File

@@ -84,7 +84,7 @@
{#if keys.length > 0} {#if keys.length > 0}
{#each keys as argName (argName)} {#each keys as argName (argName)}
{#if (!filter || filter.includes(argName)) && Object.keys(schema.properties ?? {}).includes(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 <InputTransformForm
{previousModuleId} {previousModuleId}
bind:arg={args[argName]} bind:arg={args[argName]}
@@ -128,7 +128,7 @@
> >
<div <div
slot="submission" 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 <Button
variant="border" variant="border"

View File

@@ -405,6 +405,15 @@
editor && editor.dispose() editor && editor.dispose()
} catch (err) {} } catch (err) {}
}) })
export function setCursorToEnd(): void {
if (editor) {
const lastLine = editor.getModel()?.getLineCount() ?? 1
const lastColumn = editor.getModel()?.getLineMaxColumn(lastLine) ?? 1
editor.setPosition({ lineNumber: lastLine, column: lastColumn })
editor.focus()
}
}
</script> </script>
<EditorTheme /> <EditorTheme />

View File

@@ -0,0 +1,87 @@
<script lang="ts">
import { twMerge } from 'tailwind-merge'
export let marginWidth = '2px'
export let animationDuration = '2s'
export let baseRadius = '4px'
export let animate = true
export let wrapperClasses = ''
export let ringColor = 'transparent'
let clientWidth = 0
let clientHeight = 0
$: circleRadius = Math.ceil(
Math.sqrt(clientWidth * clientWidth + clientHeight * clientHeight) / 2
)
</script>
<div
class={twMerge('gradient-button', wrapperClasses)}
style="--margin-width: {marginWidth}; --animation-duration: {animationDuration}; --base-radius: {baseRadius}; --circle-radius: {circleRadius}; --ring-color: {ringColor}"
class:animate
bind:clientWidth
bind:clientHeight
>
<slot />
</div>
<style>
.gradient-button {
position: relative;
padding: var(--margin-width, 2px);
font-size: inherit;
border: none;
border-radius: calc(var(--base-radius) + var(--margin-width, 2px));
color: currentColor;
background: inherit;
z-index: 1;
overflow: hidden;
}
/* 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(from 0deg, #a4c7f2, #0c79fd, #104688, #a4c7f2);
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);
}
}
.gradient-button.animate:hover::before {
animation-duration: 1s;
}
</style>

View File

@@ -19,9 +19,9 @@
</script> </script>
<Popover on:close class="leading-none"> <Popover on:close class="leading-none">
<PopoverButton> <PopoverButton let:open>
<div use:floatingRef> <div use:floatingRef>
<slot name="button" /> <slot name="button" {open} />
</div> </div>
</PopoverButton> </PopoverButton>
<ConditionalPortal condition={shouldUsePortal} {target}> <ConditionalPortal condition={shouldUsePortal} {target}>

View File

@@ -4,11 +4,13 @@
import FlowModuleSchemaMap from './map/FlowModuleSchemaMap.svelte' import FlowModuleSchemaMap from './map/FlowModuleSchemaMap.svelte'
import WindmillIcon from '../icons/WindmillIcon.svelte' import WindmillIcon from '../icons/WindmillIcon.svelte'
import { Skeleton } from '../common' import { Skeleton } from '../common'
import { getContext } from 'svelte' import { getContext, setContext } from 'svelte'
import type { FlowEditorContext } from './types' import type { FlowEditorContext } from './types'
import type { FlowCopilotContext } from '../copilot/flow' import type { FlowCopilotContext } from '../copilot/flow'
import { classNames } from '$lib/utils' import { classNames } from '$lib/utils'
import { writable } from 'svelte/store'
import type { PropPickerWrapperContext, PropPickerConfig } from '$lib/components/prop_picker'
import type { PickableProperties } from '$lib/components/flows/previousResults'
const { flowStore } = getContext<FlowEditorContext>('FlowEditorContext') const { flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
export let loading: boolean export let loading: boolean
@@ -24,6 +26,23 @@
const { currentStepStore: copilotCurrentStepStore } = const { currentStepStore: copilotCurrentStepStore } =
getContext<FlowCopilotContext>('FlowCopilotContext') getContext<FlowCopilotContext>('FlowCopilotContext')
const propPickerConfig = writable<PropPickerConfig | undefined>(undefined)
setContext<PropPickerWrapperContext>('PropPickerWrapper', {
propPickerConfig,
inputMatches: writable(undefined),
focusProp: (propName, insertionMode, onSelect) => {
propPickerConfig.set({
propName,
insertionMode,
onSelect
})
},
clearFocus: () => {
propPickerConfig.set(undefined)
},
filteredPickableProperties: writable<PickableProperties | undefined>(undefined)
})
</script> </script>
<div <div

View File

@@ -4,7 +4,7 @@
import { Alert, Badge } from '$lib/components/common' import { Alert, Badge } from '$lib/components/common'
import type { FlowModule, FlowModuleValue, InputTransform, PathScript, RawScript } from '$lib/gen' import type { FlowModule, FlowModuleValue, InputTransform, PathScript, RawScript } from '$lib/gen'
import { getContext, setContext } from 'svelte' import { getContext, setContext } from 'svelte'
import type { PropPickerWrapperContext } from '../propPicker/PropPickerWrapper.svelte' import type { PropPickerWrapperContext } from '$lib/components/prop_picker'
import { writable } from 'svelte/store' import { writable } from 'svelte/store'
import Toggle from '../../Toggle.svelte' import Toggle from '../../Toggle.svelte'
import InputTransformSchemaForm from '$lib/components/InputTransformSchemaForm.svelte' import InputTransformSchemaForm from '$lib/components/InputTransformSchemaForm.svelte'
@@ -79,7 +79,9 @@
setContext<PropPickerWrapperContext>('PropPickerWrapper', { setContext<PropPickerWrapperContext>('PropPickerWrapper', {
focusProp: () => {}, focusProp: () => {},
propPickerConfig: writable(undefined), propPickerConfig: writable(undefined),
clearFocus: () => {} inputMatches: writable(undefined),
clearFocus: () => {},
filteredPickableProperties: writable(undefined)
}) })
</script> </script>

View File

@@ -378,10 +378,11 @@
class={advancedSelected === 'runtime' ? 'h-[calc(100%-68px)]' : 'h-[calc(100%-34px)]'} 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')} {#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 <PropPickerWrapper
pickableProperties={stepPropPicker.pickableProperties} pickableProperties={stepPropPicker.pickableProperties}
error={failureModule} error={failureModule}
noPadding
> >
<InputTransformSchemaForm <InputTransformSchemaForm
bind:this={inputTransformSchemaForm} bind:this={inputTransformSchemaForm}

View File

@@ -28,7 +28,8 @@
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte' import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
import { getDependeeAndDependentComponents } from '../flowExplorer' import { getDependeeAndDependentComponents } from '../flowExplorer'
import { replaceId } from '../flowStore' import { replaceId } from '../flowStore'
import FlowPropPicker from '$lib/components/flows/propPicker/FlowPropPicker.svelte'
import type { PropPickerWrapperContext } from '$lib/components/prop_picker'
export let selected: boolean = false export let selected: boolean = false
export let deletable: boolean = false export let deletable: boolean = false
export let retry: boolean = false export let retry: boolean = false
@@ -46,17 +47,24 @@
export let concurrency: boolean = false export let concurrency: boolean = false
export let retries: number | undefined = undefined export let retries: number | undefined = undefined
export let warningMessage: string | undefined = undefined export let warningMessage: string | undefined = undefined
let pickableIds: Record<string, any> | undefined = undefined
const { flowInputsStore } = getContext<{ flowInputsStore: Writable<FlowInput | undefined> }>( const { flowInputsStore } = getContext<{ flowInputsStore: Writable<FlowInput | undefined> }>(
'FlowGraphContext' 'FlowGraphContext'
) )
const flowEditorContext = getContext<FlowEditorContext>('FlowEditorContext') const flowEditorContext = getContext<FlowEditorContext>('FlowEditorContext')
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const { currentStepStore: copilotCurrentStepStore } = const { currentStepStore: copilotCurrentStepStore } =
getContext<FlowCopilotContext | undefined>('FlowCopilotContext') || {} getContext<FlowCopilotContext | undefined>('FlowCopilotContext') || {}
const { propPickerConfig, filteredPickableProperties } =
getContext<PropPickerWrapperContext>('PropPickerWrapper')
$: filteredPickableProperties && (pickableIds = $filteredPickableProperties?.priorIds)
let editId = false let editId = false
let newId: string = id ?? '' let newId: string = id ?? ''
@@ -266,6 +274,19 @@ hover:border-blue-700 hover:!visible {hover ? '' : '!hidden'}"
{/if} {/if}
</div> </div>
</div> </div>
{#if id && $propPickerConfig && pickableIds && Object.keys(pickableIds).includes(id)}
<div class="absolute -bottom-[18px] right-[50%] translate-x-[50%]">
<FlowPropPicker
json={{
[id]: pickableIds[id]
}}
prefix={'results'}
viewOnly={false}
/>
</div>
{/if}
{#if deletable} {#if deletable}
<button <button
class="absolute -top-[10px] -right-[10px] rounded-full h-[20px] w-[20px] trash center-center text-secondary class="absolute -top-[10px] -right-[10px] rounded-full h-[20px] w-[20px] trash center-center text-secondary
@@ -280,7 +301,7 @@ hover:border-blue-700 hover:!visible {hover ? '' : '!hidden'}"
{#if id !== 'preprocessor'} {#if id !== 'preprocessor'}
<button <button
class="absolute -top-[10px] right-[60px] rounded-full h-[20px] w-[20px] trash center-center text-secondary class="absolute -top-[10px] right-[60px] rounded-full h-[20px] w-[20px] center-center text-secondary
outline-[1px] outline dark:outline-gray-500 outline-gray-300 bg-surface duration-150 hover:bg-blue-400 hover:text-white outline-[1px] outline dark:outline-gray-500 outline-gray-300 bg-surface duration-150 hover:bg-blue-400 hover:text-white
{hover ? '' : '!hidden'}" {hover ? '' : '!hidden'}"
on:click|preventDefault|stopPropagation={(event) => dispatch('move')} on:click|preventDefault|stopPropagation={(event) => dispatch('move')}

View File

@@ -4,6 +4,8 @@
import { classNames } from '$lib/utils' import { classNames } from '$lib/utils'
import { createEventDispatcher, getContext } from 'svelte' import { createEventDispatcher, getContext } from 'svelte'
import type { FlowCopilotContext } from '$lib/components/copilot/flow' import type { FlowCopilotContext } from '$lib/components/copilot/flow'
import type { PropPickerConfig } from '$lib/components/prop_picker'
import FlowPropPicker from '$lib/components/flows/propPicker/FlowPropPicker.svelte'
export let label: string | undefined = undefined export let label: string | undefined = undefined
export let bgColor: string = '' export let bgColor: string = ''
@@ -14,6 +16,9 @@
export let borderColor: string | undefined = undefined export let borderColor: string | undefined = undefined
export let hideId: boolean = false export let hideId: boolean = false
export let preLabel: string | undefined = undefined export let preLabel: string | undefined = undefined
export let propPickerConfig: PropPickerConfig | undefined = undefined
export let inputJson = {}
export let prefix = ''
const dispatch = createEventDispatcher<{ const dispatch = createEventDispatcher<{
insert: { insert: {
@@ -33,7 +38,7 @@
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div <div
class={classNames( class={classNames(
'w-full flex relative overflow-hidden rounded-sm', 'w-full flex relative rounded-sm',
selectable ? 'cursor-pointer' : '', selectable ? 'cursor-pointer' : '',
selected ? 'outline outline-offset-1 outline-2 outline-gray-600' : '', selected ? 'outline outline-offset-1 outline-2 outline-gray-600' : '',
label === 'Input' && $copilotCurrentStepStore === 'Input' ? 'z-[901]' : '' label === 'Input' && $copilotCurrentStepStore === 'Input' ? 'z-[901]' : ''
@@ -76,4 +81,9 @@
{/if} {/if}
</div> </div>
</div> </div>
{#if propPickerConfig && Object.keys(inputJson).length > 0}
<div class="absolute -bottom-[18px] right-[50%] translate-x-[50%]">
<FlowPropPicker json={inputJson} {prefix} />
</div>
{/if}
</div> </div>

View File

@@ -260,3 +260,42 @@ declare const approvers: string
} }
` `
} }
export function buildPrefixRegex(words: string[]): Array<{ regex: RegExp; word: string }> {
return words.map((word) => {
const prefixes: string[] = []
for (let i = 1; i <= word.length; i++) {
prefixes.push(word.slice(0, i) + '$')
}
prefixes.push(word + '\\.')
prefixes.push(word + '\\[')
return {
regex: new RegExp(`^(${prefixes.join('|')}).*`),
word
}
})
}
export function filterNestedObject(obj: any, nestedKeys: string[]) {
if (nestedKeys.length === 0) return obj
if (nestedKeys.length === 1) {
if (nestedKeys[0] === '') {
return obj
}
const regexes = buildPrefixRegex(Object.keys(obj))
const matches = regexes.filter(({ regex }) => regex.test(nestedKeys[0]))
const filteredObj = {}
matches.forEach(({ word }) => {
if (obj.hasOwnProperty(word)) {
filteredObj[word] = obj[word]
}
})
return filteredObj
}
const [key, ...rest] = nestedKeys
if (obj && typeof obj === 'object' && key in obj) {
return filterNestedObject(obj[key], rest)
}
return undefined
}

View File

@@ -0,0 +1,64 @@
<script lang="ts">
import ObjectViewer from '$lib/components/propertyPicker/ObjectViewer.svelte'
import AnimatedButton from '$lib/components/common/button/AnimatedButton.svelte'
import { Popup } from '$lib/components/common'
import { Plug } from 'lucide-svelte'
import { twMerge } from 'tailwind-merge'
import type { PropPickerWrapperContext } from '$lib/components/prop_picker'
import { getContext } from 'svelte'
import Popover from '$lib/components/Popover.svelte'
export let json = {}
export let prefix = ''
export let viewOnly = false
const { propPickerConfig } = getContext<PropPickerWrapperContext>('PropPickerWrapper')
</script>
<button
on:click|preventDefault|stopPropagation={(e) => {
e.preventDefault()
e.stopPropagation()
}}
on:keydown|preventDefault|stopPropagation
data-prop-picker
>
<AnimatedButton
animate={$propPickerConfig?.insertionMode === 'connect' && !viewOnly}
wrapperClasses="h-[20px] w-[20px] "
baseRadius="9999px"
marginWidth="1px"
>
<Popup floatingConfig={{ strategy: 'fixed', placement: 'bottom-start' }}>
<svelte:fragment slot="button" let:open>
<Popover disablePopup={open}>
<svelte:fragment slot="text">node outputs</svelte:fragment>
<button
class={twMerge(
'rounded-full trash center-center h-[18px] w-[18px]',
viewOnly
? 'outline-[1px] outline dark:outline-gray-500 outline-gray-300 duration-150 bg-surface hover:bg-surface-hover text-secondary'
: $propPickerConfig?.insertionMode == 'connect'
? 'bg-surface text-blue-500'
: 'outline-[1px] outline dark:outline-gray-500 outline-gray-300 duration-150 bg-blue-500 hover:bg-blue-700 text-white'
)}
>
<Plug size={12} strokeWidth={2} />
</button>
</Popover>
</svelte:fragment>
<div data-prop-picker>
<ObjectViewer
{json}
topBrackets={false}
pureViewer={viewOnly}
{prefix}
on:select={(e) => {
$propPickerConfig?.onSelect(e.detail)
$propPickerConfig = undefined
}}
/>
</div>
</Popup>
</AnimatedButton>
</button>

View File

@@ -1,31 +1,13 @@
<script context="module" lang="ts">
type InsertionMode = 'append' | 'connect' | 'insert'
type SelectCallback = (path: string) => boolean
type PropPickerConfig = {
insertionMode: InsertionMode
propName: string
onSelect: SelectCallback
}
export type PropPickerWrapperContext = {
propPickerConfig: Writable<PropPickerConfig | undefined>
focusProp: (propName: string, insertionMode: InsertionMode, onSelect: SelectCallback) => void
clearFocus: () => void
}
</script>
<script lang="ts"> <script lang="ts">
import PropPicker from '$lib/components/propertyPicker/PropPicker.svelte' import PropPicker from '$lib/components/propertyPicker/PropPicker.svelte'
import PropPickerResult from '$lib/components/propertyPicker/PropPickerResult.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 { createEventDispatcher, getContext } from 'svelte'
import { Pane, Splitpanes } from 'svelte-splitpanes' import { Pane, Splitpanes } from 'svelte-splitpanes'
import { writable, type Writable } from 'svelte/store'
import type { PickableProperties } from '../previousResults' import type { PickableProperties } from '../previousResults'
import { twMerge } from 'tailwind-merge' import { twMerge } from 'tailwind-merge'
import AnimatedButton from '$lib/components/common/button/AnimatedButton.svelte'
import type { PropPickerWrapperContext } from '$lib/components/prop_picker'
export let pickableProperties: PickableProperties | undefined export let pickableProperties: PickableProperties | undefined
export let result: any = undefined export let result: any = undefined
export let extraResults: any = undefined export let extraResults: any = undefined
@@ -35,74 +17,98 @@
export let notSelectable = false export let notSelectable = false
export let noPadding: boolean = false export let noPadding: boolean = false
const propPickerConfig = writable<PropPickerConfig | undefined>(undefined)
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
setContext<PropPickerWrapperContext>('PropPickerWrapper', { const { propPickerConfig } = getContext<PropPickerWrapperContext>('PropPickerWrapper')
propPickerConfig,
focusProp: (propName, insertionMode, onSelect) => { async function getPropPickerElements(): Promise<HTMLElement[]> {
propPickerConfig.set({ return Array.from(
propName, document.querySelectorAll('[data-prop-picker], [data-prop-picker] *')
insertionMode, ) as HTMLElement[]
onSelect }
})
},
clearFocus: () => {
propPickerConfig.set(undefined)
}
})
</script> </script>
<div <div
class="h-full w-full" class="h-full w-full"
use:clickOutside data-prop-picker-root
on:click_outside={() => propPickerConfig.set(undefined)} use:clickOutside={{ capture: true, exclude: getPropPickerElements }}
on:click_outside={() => {
propPickerConfig.set(undefined)
}}
> >
<Splitpanes> <Splitpanes class={$propPickerConfig ? 'splitpanes-remove-splitter' : ''}>
<Pane <Pane
minSize={20} minSize={20}
size={60} size={60}
class={twMerge('relative !transition-none', noPadding ? '' : 'p-2')} class={twMerge('relative !transition-none ', noPadding ? '' : 'p-2')}
> >
<slot /> <slot />
</Pane> </Pane>
<Pane <Pane
minSize={20} minSize={20}
size={40} size={40}
class="pt-2 relative !transition-none {$propPickerConfig ? 'border-2 border-blue-500' : ''}" class="!transition-none z-1000 {$propPickerConfig ? 'ml-[-1px]' : ''}"
> >
{#if result} <AnimatedButton
<PropPickerResult animate={$propPickerConfig?.insertionMode == 'connect'}
{result} baseRadius="4px"
{extraResults} wrapperClasses="h-full w-full pt-2"
{flow_input} marginWidth="4px"
on:select={({ detail }) => { ringColor={$propPickerConfig?.insertionMode == 'insert' ||
if (!notSelectable && !$propPickerConfig) { $propPickerConfig?.insertionMode == 'append'
sendUserToast('Set cursor within an input or click on the plug first', true) ? '#3b82f6'
} : 'transparent'}
dispatch('select', detail) animationDuration="1s"
if ($propPickerConfig?.onSelect(detail)) { >
propPickerConfig.set(undefined) {#if result}
} <PropPickerResult
}} {result}
/> {extraResults}
{:else if pickableProperties} {flow_input}
<PropPicker allowCopy={!notSelectable && !$propPickerConfig}
{displayContext} on:select={({ detail }) => {
{error} dispatch('select', detail)
{pickableProperties} if ($propPickerConfig?.onSelect(detail)) {
{notSelectable} propPickerConfig.set(undefined)
on:select={({ detail }) => { }
if (!notSelectable && !$propPickerConfig) { }}
sendUserToast('Set cursor within an input or click on the plug first', true) />
} {:else if pickableProperties}
dispatch('select', detail) <PropPicker
if ($propPickerConfig?.onSelect(detail)) { {displayContext}
propPickerConfig.set(undefined) {error}
} {pickableProperties}
}} {notSelectable}
/> allowCopy={!notSelectable && !$propPickerConfig}
{/if} on:select={({ detail }) => {
dispatch('select', detail)
if ($propPickerConfig?.onSelect(detail)) {
propPickerConfig.set(undefined)
}
}}
/>
{/if}
</AnimatedButton>
</Pane> </Pane>
</Splitpanes> </Splitpanes>
</div> </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;
}
:global(.splitpanes__pane) {
overflow-y: auto;
scrollbar-width: none;
}
:global(.splitpanes__pane:hover) {
scrollbar-width: thin;
}
</style>

View File

@@ -5,7 +5,8 @@
import { getStateColor } from '../../util' import { getStateColor } from '../../util'
import type { GraphModuleState } from '../../model' import type { GraphModuleState } from '../../model'
import type { GraphEventHandlers } from '../../graphBuilder' import type { GraphEventHandlers } from '../../graphBuilder'
import { getContext } from 'svelte'
import type { PropPickerWrapperContext } from '$lib/components/prop_picker'
export let data: { export let data: {
offset: number offset: number
id: string id: string
@@ -13,11 +14,21 @@
flowModuleStates: Record<string, GraphModuleState> | undefined flowModuleStates: Record<string, GraphModuleState> | undefined
eventHandlers: GraphEventHandlers eventHandlers: GraphEventHandlers
} }
const { propPickerConfig, filteredPickableProperties } =
getContext<PropPickerWrapperContext>('PropPickerWrapper')
$: filteredInput = filterIterFromInput($filteredPickableProperties?.flow_input)
function filterIterFromInput(inputJson: Record<string, any> | undefined): Record<string, any> {
if (!inputJson || typeof inputJson !== 'object' || !inputJson.iter) return {}
return { iter: inputJson.iter }
}
</script> </script>
<NodeWrapper let:darkMode offset={data.offset}> <NodeWrapper let:darkMode offset={data.offset}>
<VirtualItem <VirtualItem
label={'Do one iteration'} label={'Do one iterations'}
selectable={false} selectable={false}
selected={false} selected={false}
id={data.id} id={data.id}
@@ -27,5 +38,8 @@
on:select={(e) => { on:select={(e) => {
data?.eventHandlers?.select(e.detail) data?.eventHandlers?.select(e.detail)
}} }}
propPickerConfig={$propPickerConfig}
inputJson={filteredInput}
prefix="flow_input"
/> />
</NodeWrapper> </NodeWrapper>

View File

@@ -7,6 +7,7 @@
import { getContext } from 'svelte' import { getContext } from 'svelte'
import type { Writable } from 'svelte/store' import type { Writable } from 'svelte/store'
import InsertModuleButton from '$lib/components/flows/map/InsertModuleButton.svelte' import InsertModuleButton from '$lib/components/flows/map/InsertModuleButton.svelte'
import type { PropPickerWrapperContext } from '$lib/components/prop_picker'
export let data: { export let data: {
hasPreprocessor: boolean hasPreprocessor: boolean
@@ -23,6 +24,20 @@
const { selectedId } = getContext<{ const { selectedId } = getContext<{
selectedId: Writable<string | undefined> selectedId: Writable<string | undefined>
}>('FlowGraphContext') }>('FlowGraphContext')
const { propPickerConfig, filteredPickableProperties } =
getContext<PropPickerWrapperContext>('PropPickerWrapper')
function filterIterFromInput(inputJson: Record<string, any> | undefined): Record<string, any> {
if (!inputJson || typeof inputJson !== 'object') return {}
const newJson = { ...inputJson }
delete newJson.iter
return newJson
}
$: filteredInput = filterIterFromInput($filteredPickableProperties?.flow_input)
</script> </script>
<NodeWrapper let:darkMode> <NodeWrapper let:darkMode>
@@ -64,5 +79,8 @@
on:select={(e) => { on:select={(e) => {
data.eventHandlers?.select(e.detail) data.eventHandlers?.select(e.detail)
}} }}
propPickerConfig={$propPickerConfig}
inputJson={filteredInput}
prefix="flow_input"
/> />
</NodeWrapper> </NodeWrapper>

View File

@@ -0,0 +1,20 @@
import type { Writable } from 'svelte/store'
import type { PickableProperties } from '$lib/components/flows/previousResults'
type InsertionMode = 'append' | 'connect' | 'insert'
type SelectCallback = (path: string) => boolean
export type PropPickerConfig = {
insertionMode: InsertionMode
propName: string
onSelect: SelectCallback
}
export type PropPickerWrapperContext = {
propPickerConfig: Writable<PropPickerConfig | undefined>
filteredPickableProperties: Writable<PickableProperties | undefined>
inputMatches: Writable<{ word: string; value: string }[] | undefined>
focusProp: (propName: string, insertionMode: InsertionMode, onSelect: SelectCallback) => void
clearFocus: () => void
}

View File

@@ -1,13 +1,12 @@
<script lang="ts"> <script lang="ts">
import { copyToClipboard, pluralize, truncate } from '$lib/utils' import { copyToClipboard, truncate } from '$lib/utils'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import { Badge } from '../common'
import { computeKey } from './utils' import { computeKey } from './utils'
import WarningMessage from './WarningMessage.svelte'
import { NEVER_TESTED_THIS_FAR } from '../flows/models' import { NEVER_TESTED_THIS_FAR } from '../flows/models'
import Portal from '$lib/components/Portal.svelte' import Portal from '$lib/components/Portal.svelte'
import { Button } from '$lib/components/common'
import Popover from '$lib/components/Popover.svelte'
import { Download, PanelRightOpen } from 'lucide-svelte' import { Download, PanelRightOpen } from 'lucide-svelte'
import S3FilePicker from '../S3FilePicker.svelte' import S3FilePicker from '../S3FilePicker.svelte'
import { workspaceStore } from '$lib/stores' import { workspaceStore } from '$lib/stores'
@@ -19,9 +18,9 @@
export let collapsed = (level != 0 && level % 3 == 0) || Array.isArray(json) export let collapsed = (level != 0 && level % 3 == 0) || Array.isArray(json)
export let rawKey = false export let rawKey = false
export let topBrackets = false export let topBrackets = false
export let topLevelNode = false
export let allowCopy = true export let allowCopy = true
export let collapseLevel: number | undefined = undefined export let collapseLevel: number | undefined = undefined
export let prefix = ''
let s3FileViewer: S3FilePicker let s3FileViewer: S3FilePicker
@@ -50,12 +49,21 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
function selectProp(key: string, value: any | undefined = undefined) { function computeFullKey(key: string, rawKey: boolean) {
if (pureViewer && allowCopy) { if (rawKey) {
const valueToCopy = value !== undefined ? value : computeKey(key, isArray, currentPath) return `${prefix}('${key}')`
copyToClipboard(valueToCopy)
} }
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 $: keyLimit = isArray ? 1 : 100
@@ -73,33 +81,40 @@
{#if level != 0 && keys.length > 1} {#if level != 0 && keys.length > 1}
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<span class="cursor-pointer border hover:bg-surface-hover px-1 rounded" on:click={collapse}> <Button
- color="light"
</span> size="xs2"
variant="border"
on:click={collapse}
wrapperClasses="inline-flex w-fit h-5"
btnClasses="font-semibold text-primary border-nord-300 rounded-[0.275rem]">-</Button
>
{/if} {/if}
{#if level == 0 && topBrackets}<span class="h-0">{openBracket}</span>{/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 pl-2 ${level === 0 ? 'border-none' : 'border-l border-dotted'}`}>
{#each keys.length > keyLimit ? keys.slice(0, keyLimit) : keys as key, index (key)} {#each keys.length > keyLimit ? keys.slice(0, keyLimit) : keys as key, index (key)}
<li> <li>
<button on:click={() => selectProp(key)} class="whitespace-nowrap"> <Popover>
{#if topLevelNode} <svelte:fragment slot="text">{computeFullKey(key, rawKey)}</svelte:fragment>
<Badge baseClass="border border-blue-600" color="indigo">{key}</Badge> <Button
{:else} on:click={() => selectProp(key)}
<span size="xs2"
class="key {pureViewer color="dark"
? 'cursor-auto' variant="contained"
: 'border '} font-semibold rounded px-1 hover:bg-surface-hover text-2xs text-secondary" wrapperClasses="inline-flex p-0 whitespace-nowrap w-fit h-4"
> btnClasses="font-normal rounded-[0.275rem]"
{!isArray ? key : index}</span >
> <span class={pureViewer ? 'cursor-auto' : ''}>
{/if}: {!isArray ? key : index}
</button> </span>
</Button>
</Popover>
:
{#if getTypeAsString(json[key]) === 'object'} {#if getTypeAsString(json[key]) === 'object'}
<svelte:self <svelte:self
json={json[key]} json={json[key]}
level={level + 1} level={level + 1}
currentPath={computeKey(key, isArray, currentPath)} currentPath={computeFullKey(key, isArray)}
{pureViewer} {pureViewer}
{allowCopy} {allowCopy}
on:select on:select
@@ -107,28 +122,38 @@
collapsed={collapseLevel !== undefined ? level + 1 >= collapseLevel : undefined} collapsed={collapseLevel !== undefined ? level + 1 >= collapseLevel : undefined}
/> />
{:else} {:else}
<button <Popover disablePopup={!json[key]}>
class="val text-left {pureViewer <svelte:fragment slot="text">
? 'cursor-auto' {JSON.stringify(json[key])}
: ''} rounded px-1 hover:bg-blue-100 dark:hover:bg-blue-100/10 {getTypeAsString( </svelte:fragment>
json[key] <button
)}" class="val text-left {pureViewer
on:click={() => selectProp(key, json[key])} ? 'cursor-auto'
> : ''} rounded px-1 {getTypeAsString(json[key])}"
{#if json[key] === NEVER_TESTED_THIS_FAR} on:click={() => {
<WarningMessage /> if (json[key]) {
{:else if json[key] == undefined} copyToClipboard(json[key])
<span class="text-2xs">undefined</span> }
{:else if json[key] == null} }}
<span class="text-2xs">null</span> disabled={false}
{:else if typeof json[key] == 'string'} >
<span title={json[key]} class="text-2xs">"{truncate(json[key], 200)}"</span> {#if json[key] === NEVER_TESTED_THIS_FAR}
{:else} <span class="text-2xs text-tertiary font-normal">
<span title={JSON.stringify(json[key])} class="text-2xs"> Test the flow to see a value
{truncate(JSON.stringify(json[key]), 200)} </span>
</span> {:else if json[key] == undefined}
{/if} <span class="text-2xs">undefined</span>
</button> {:else if json[key] == null}
<span class="text-2xs">null</span>
{:else if typeof json[key] == 'string'}
<span class="text-2xs">"{truncate(json[key], 200)}"</span>
{:else}
<span class="text-2xs">
{truncate(JSON.stringify(json[key]), 200)}
</span>
{/if}
</button>
</Popover>
{/if} {/if}
</li> </li>
{/each} {/each}
@@ -167,17 +192,18 @@
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- 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} {#if fullyCollapsed}
<span class="text-tertiary text-xs"> <Button
{pluralize(Object.keys(json).length, Array.isArray(json) ? 'item' : 'key')} color="light"
</span> size="xs2"
variant="border"
on:click={collapse}
wrapperClasses="inline-flex w-fit h-5"
btnClasses="font-semibold border-nord-300 rounded-[0.275rem] p-1"
>
{openBracket}{collapsedSymbol}{closeBracket}
</Button>
{/if} {/if}
{:else if topBrackets} {:else if topBrackets}
<span class="text-primary">{openBracket}{closeBracket}</span> <span class="text-primary">{openBracket}{closeBracket}</span>

View File

@@ -3,41 +3,45 @@
import { workspaceStore } from '$lib/stores' import { workspaceStore } from '$lib/stores'
import { getContext } from 'svelte' import { getContext } from 'svelte'
import { Badge, Button } from '../common' import { Badge, Button } from '../common'
import type { PropPickerWrapperContext } from '../flows/propPicker/PropPickerWrapper.svelte' import type { PropPickerWrapperContext } from '../prop_picker'
import { createEventDispatcher } from 'svelte'
import ObjectViewer from './ObjectViewer.svelte' import ObjectViewer from './ObjectViewer.svelte'
import { keepByKey } from './utils' import { keepByKey } from './utils'
import type { PickableProperties } from '../flows/previousResults' import type { PickableProperties } from '../flows/previousResults'
import ClearableInput from '../common/clearableInput/ClearableInput.svelte' import ClearableInput from '../common/clearableInput/ClearableInput.svelte'
import { filterNestedObject } from '../flows/previousResults'
export let pickableProperties: PickableProperties export let pickableProperties: PickableProperties
export let displayContext = true export let displayContext = true
export let notSelectable: boolean export let notSelectable: boolean
export let error: boolean = false export let error: boolean = false
export let allowCopy = false
$: previousId = pickableProperties?.previousId $: previousId = pickableProperties?.previousId
let variables: Record<string, string> = {} let variables: Record<string, string> = {}
let resources: Record<string, any> = {} let resources: Record<string, any> = {}
let displayVariable = false let displayVariable = false
let displayResources = false let displayResources = false
let allResultsCollapsed = true
const dispatch = createEventDispatcher() let flowInputsFiltered: Record<string, any> = {}
let resultByIdFiltered: Record<string, any> = {}
let collapsableInitialState:
| {
allResultsCollapsed: boolean
displayVariable: boolean
displayResources: boolean
}
| undefined
const EMPTY_STRING = '' const EMPTY_STRING = ''
let search = '' let search = ''
const { propPickerConfig } = getContext<PropPickerWrapperContext>('PropPickerWrapper') const { propPickerConfig, filteredPickableProperties, inputMatches } =
getContext<PropPickerWrapperContext>('PropPickerWrapper')
$: flowInputsFiltered = $filteredPickableProperties = { ...pickableProperties }
search === EMPTY_STRING
? pickableProperties.flow_input
: keepByKey(pickableProperties.flow_input, search)
$: resultByIdFiltered = $: filterPickableProperties(), updateCollapsable(), search, $inputMatches
search === EMPTY_STRING
? pickableProperties.priorIds
: keepByKey(pickableProperties.priorIds, search)
$: suggestedPropsFiltered = $propPickerConfig $: suggestedPropsFiltered = $propPickerConfig
? keepByKey(pickableProperties.priorIds, $propPickerConfig.propName) ? keepByKey(pickableProperties.priorIds, $propPickerConfig.propName)
@@ -62,23 +66,90 @@
).map((resource) => [resource.path, resource.description ?? '']) ).map((resource) => [resource.path, resource.description ?? ''])
) )
} }
function filterPickableProperties() {
flowInputsFiltered = pickableProperties.flow_input
resultByIdFiltered = pickableProperties.priorIds
if ($inputMatches) {
if (!$inputMatches.some((match) => match.word === 'flow_input')) {
flowInputsFiltered = []
}
if (!$inputMatches.some((match) => match.word === 'results')) {
resultByIdFiltered = []
}
if ($inputMatches.length == 1) {
if ($inputMatches[0].word === 'flow_input') {
let [, ...nestedKeys] = $inputMatches[0].value.split('.')
flowInputsFiltered = filterNestedObject(flowInputsFiltered, nestedKeys)
} else if ($inputMatches[0].word === 'results') {
let [, ...nestedKeys] = $inputMatches[0].value.split('.')
resultByIdFiltered = filterNestedObject(resultByIdFiltered, nestedKeys)
}
}
}
if (flowInputsFiltered && search !== EMPTY_STRING) {
flowInputsFiltered = keepByKey(flowInputsFiltered, search)
}
if (resultByIdFiltered && search !== EMPTY_STRING) {
resultByIdFiltered = keepByKey(resultByIdFiltered, search)
}
if ($filteredPickableProperties) {
resultByIdFiltered && ($filteredPickableProperties.priorIds = resultByIdFiltered)
flowInputsFiltered && ($filteredPickableProperties.flow_input = flowInputsFiltered)
}
}
async function updateCollapsable() {
if (!$inputMatches || $inputMatches.length !== 1) {
resetCollapsable()
return
}
if (!collapsableInitialState) {
collapsableInitialState = { allResultsCollapsed, displayVariable, displayResources }
}
if ($inputMatches[0].word === 'variable') {
await loadVariables()
displayVariable = true
return
}
if ($inputMatches[0].word === 'resource') {
await loadResources()
displayResources = true
return
}
if ($inputMatches[0].word === 'results') {
allResultsCollapsed = false
return
}
}
function resetCollapsable() {
if (!collapsableInitialState) {
return
}
;({ allResultsCollapsed, displayVariable, displayResources } = collapsableInitialState)
collapsableInitialState = undefined
}
</script> </script>
<div class="flex flex-col h-full"> <div class="flex flex-col h-full !bg-surface rounded overflow-hidden">
<div class="px-2"> <div class="px-2">
{#if !notSelectable} {#if !notSelectable}
<div class="flex flex-row space-x-1"> {#if $propPickerConfig}
{#if $propPickerConfig} <!-- <Badge large color="blue">
<Badge large color="blue"> {`Selected: ${$propPickerConfig?.propName}`}
{`Selected: ${$propPickerConfig?.propName}`} </Badge> -->
</Badge> <Badge large color="blue">
<Badge large color="blue"> {`Mode: ${$propPickerConfig?.insertionMode}`}
{`Mode: ${$propPickerConfig?.insertionMode}`} </Badge>
</Badge> {:else}
{:else} <Badge large color="blue">&leftarrow; Edit or connect an input</Badge>
<Badge large color="blue">&leftarrow; Edit or connect an input</Badge> {/if}
{/if}
</div>
{/if} {/if}
<ClearableInput bind:value={search} placeholder="Search prop..." wrapperClass="py-2" /> <ClearableInput bind:value={search} placeholder="Search prop..." wrapperClass="py-2" />
</div> </div>
@@ -86,28 +157,26 @@
class="overflow-y-auto px-2 pt-2 grow" class="overflow-y-auto px-2 pt-2 grow"
class:bg-surface-secondary={!$propPickerConfig && !notSelectable} class:bg-surface-secondary={!$propPickerConfig && !notSelectable}
> >
<div class="flex justify-between items-center space-x-1"> {#if flowInputsFiltered && Object.keys(flowInputsFiltered).length > 0}
<span class="font-bold text-sm">Flow Input</span> <div class="flex justify-between items-center space-x-1">
<div class="flex space-x-2 items-center" /> <span class="font-normal text-sm text-secondary">Flow Input</span>
</div> <div class="flex space-x-2 items-center" />
<div class="overflow-y-auto mb-2"> </div>
<ObjectViewer
allowCopy={false}
pureViewer={!$propPickerConfig}
json={flowInputsFiltered}
on:select={(e) => {
dispatch(
'select',
e.detail?.startsWith('[') ? `flow_input${e.detail}` : `flow_input.${e.detail}`
)
}}
/>
</div>
{#if error}
<span class="font-bold text-sm">Error</span>
<div class="overflow-y-auto mb-2"> <div class="overflow-y-auto mb-2">
<ObjectViewer <ObjectViewer
allowCopy={false} {allowCopy}
pureViewer={!$propPickerConfig}
json={flowInputsFiltered}
prefix="flow_input"
on:select
/>
</div>
{/if}
{#if error}
<span class="font-normal text-sm text-secondary">Error</span>
<div class="overflow-y-auto mb-2">
<ObjectViewer
{allowCopy}
pureViewer={!$propPickerConfig} pureViewer={!$propPickerConfig}
json={{ json={{
error: { error: {
@@ -122,160 +191,186 @@
</div> </div>
{#if Object.keys(pickableProperties.priorIds).length > 0} {#if Object.keys(pickableProperties.priorIds).length > 0}
{#if suggestedPropsFiltered && Object.keys(suggestedPropsFiltered).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"> <div class="overflow-y-auto mb-2">
<ObjectViewer <ObjectViewer
allowCopy={false} {allowCopy}
topLevelNode
pureViewer={!$propPickerConfig} pureViewer={!$propPickerConfig}
collapsed={false} collapsed={false}
json={suggestedPropsFiltered} json={suggestedPropsFiltered}
on:select={(e) => { prefix="results"
dispatch('select', `results.${e.detail}`) on:select
}}
/> />
</div> </div>
{/if} {/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"> <div class="overflow-y-auto mb-2">
<ObjectViewer <ObjectViewer
allowCopy={false} {allowCopy}
topLevelNode
pureViewer={!$propPickerConfig} pureViewer={!$propPickerConfig}
collapsed={true} collapsed={true}
json={resultByIdFiltered} json={resultByIdFiltered}
on:select={(e) => { prefix="results"
dispatch('select', `results.${e.detail}`) on:select
}}
/> />
</div> </div>
{/if} {/if}
{:else} {:else}
{#if previousId} {@const json = Object.fromEntries(
<span class="font-bold text-sm">Previous Result</span> Object.entries(resultByIdFiltered).filter(([k, v]) => k == previousId)
)}
{#if previousId && Object.keys(json).length > 0}
<span class="font-normal text-sm text-secondary">Previous Result</span>
<div class="overflow-y-auto mb-2"> <div class="overflow-y-auto mb-2">
<ObjectViewer <ObjectViewer
allowCopy={false} {allowCopy}
topLevelNode
pureViewer={!$propPickerConfig} pureViewer={!$propPickerConfig}
json={Object.fromEntries( {json}
Object.entries(resultByIdFiltered).filter(([k, v]) => k == previousId) prefix="results"
)} on:select
on:select={(e) => {
dispatch('select', `results.${e.detail}`)
}}
/> />
</div> </div>
{/if} {/if}
{#if pickableProperties.hasResume} {#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"> <div class="overflow-y-auto mb-2">
<ObjectViewer <ObjectViewer
allowCopy={false} {allowCopy}
topLevelNode
pureViewer={!$propPickerConfig} pureViewer={!$propPickerConfig}
json={{ json={{
resume: 'The resume payload', resume: 'The resume payload',
resumes: 'All resume payloads from all approvers', resumes: 'All resume payloads from all approvers',
approvers: 'The list of approvers' approvers: 'The list of approvers'
}} }}
on:select={(e) => { on:select
dispatch('select', `${e.detail}`)
}}
/> />
</div> </div>
{/if} {/if}
{#if Object.keys(pickableProperties.priorIds).length > 0} {#if Object.keys(pickableProperties.priorIds).length > 0}
{#if suggestedPropsFiltered && Object.keys(suggestedPropsFiltered).length > 0} {#if !$inputMatches && 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"> <div class="overflow-y-auto mb-2">
<ObjectViewer <ObjectViewer
allowCopy={false} {allowCopy}
topLevelNode
pureViewer={!$propPickerConfig} pureViewer={!$propPickerConfig}
collapsed={false} collapsed={false}
json={suggestedPropsFiltered} json={suggestedPropsFiltered}
on:select={(e) => { prefix="results"
dispatch('select', `results.${e.detail}`) on:select
}} />
</div>
{/if}
{#if Object.keys(resultByIdFiltered).length > 0}
<div class="overflow-y-auto mb-2">
<span class="font-normal text-sm text-tertiary">All Results :</span>
{#if !allResultsCollapsed}
<Button
color="light"
size="xs2"
variant="border"
on:click={() => {
allResultsCollapsed = true
}}
wrapperClasses="inline-flex w-fit h-4"
btnClasses="font-normal text-primary border-nord-300 rounded-[0.275rem]">-</Button
>
{/if}
<ObjectViewer
{allowCopy}
pureViewer={!$propPickerConfig}
bind:collapsed={allResultsCollapsed}
json={resultByIdFiltered}
prefix="results"
on:select
/> />
</div> </div>
{/if} {/if}
<span class="font-bold text-sm">All Results</span>
<div class="overflow-y-auto mb-2">
<ObjectViewer
allowCopy={false}
topLevelNode
pureViewer={!$propPickerConfig}
collapsed={true}
json={resultByIdFiltered}
on:select={(e) => {
dispatch('select', `results.${e.detail}`)
}}
/>
</div>
{/if} {/if}
{/if} {/if}
{#if displayContext} {#if displayContext}
<span class="font-bold text-sm">Variables </span> {#if !$inputMatches || $inputMatches.some((match) => match.word === 'variable')}
<div class="overflow-y-auto mb-2"> <div class="overflow-y-auto mb-2">
{#if displayVariable} <span class="font-normal text-sm text-secondary">Variables :</span>
<div class="flex">
{#if displayVariable}
<Button <Button
color="light" color="light"
size="xs" size="xs2"
variant="border" variant="border"
on:click={() => { on:click={() => {
displayVariable = false displayVariable = false
}}>-</Button }}
wrapperClasses="inline-flex w-fit h-4"
btnClasses="font-normal text-primary border-nord-300 rounded-[0.275rem]">-</Button
> >
</div>
<ObjectViewer <ObjectViewer
allowCopy={false} {allowCopy}
pureViewer={!$propPickerConfig} pureViewer={!$propPickerConfig}
rawKey={true} rawKey={true}
json={variables} json={variables}
on:select={(e) => dispatch('select', `variable('${e.detail}')`)} prefix="variable"
/> on:select
{:else} />
<button {:else}
class="border border-blue-600 key font-normal rounded hover:bg-blue-100 px-1" <Button
on:click={async () => { color="light"
await loadVariables() size="xs2"
displayVariable = true variant="border"
}}>{'{...}'}</button on:click={async () => {
> await loadVariables()
{/if} displayVariable = true
</div> }}
<span class="font-bold text-sm">Resources</span> wrapperClasses="inline-flex w-fit h-5"
<div class="overflow-y-auto mb-2"> btnClasses="font-semibold border-nord-300 rounded-[0.275rem] p-1"
{#if displayResources} >
<Button {'{...}'}
color="light" </Button>
variant="border" {/if}
size="xs" </div>
on:click={() => { {/if}
displayResources = false {#if !$inputMatches || $inputMatches.some((match) => match.word === 'resource')}
}}>-</Button <div class="overflow-y-auto mb-2">
> <span class="font-normal text-sm text-secondary">Resources :</span>
<ObjectViewer
allowCopy={false} {#if displayResources}
pureViewer={!$propPickerConfig} <Button
rawKey={true} color="light"
json={resources} size="xs2"
on:select={(e) => dispatch('select', `resource('${e.detail}')`)} variant="border"
/> on:click={() => {
{:else} displayResources = false
<button }}
class="border border-blue-600 px-1 key font-normal rounded hover:bg-blue-100" wrapperClasses="inline-flex w-fit h-5"
on:click={async () => { btnClasses="font-semibold text-primary border-nord-300 rounded-[0.275rem]">-</Button
await loadResources() >
displayResources = true <ObjectViewer
}}>{'{...}'}</button {allowCopy}
> pureViewer={!$propPickerConfig}
{/if} rawKey={true}
</div> json={resources}
prefix="resource"
on:select
/>
{:else}
<Button
color="light"
size="xs2"
variant="border"
on:click={async () => {
await loadResources()
displayResources = true
}}
wrapperClasses="inline-flex w-fit h-5"
btnClasses="font-semibold border-nord-300 rounded-[0.275rem] p-1"
>
{'{...}'}
</Button>
{/if}
</div>
{/if}
{/if} {/if}
</div> </div>
</div> </div>

View File

@@ -1,31 +1,21 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte'
import ObjectViewer from './ObjectViewer.svelte' import ObjectViewer from './ObjectViewer.svelte'
export let allowCopy = false
export let result: any export let result: any
export let extraResults: any = undefined export let extraResults: any = undefined
export let flow_input: any = undefined export let flow_input: any = undefined
const dispatch = createEventDispatcher()
</script> </script>
<div class="w-full px-2"> <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"> <div class="overflow-y-auto mb-2 w-full">
<ObjectViewer <ObjectViewer {allowCopy} json={{ result, ...(extraResults ? extraResults : {}) }} on:select />
allowCopy={false}
json={{ result, ...(extraResults ? extraResults : {}) }}
on:select
/>
</div> </div>
{#if flow_input} {#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"> <div class="overflow-y-auto w-full">
<ObjectViewer <ObjectViewer {allowCopy} json={flow_input} prefix="flow_input" on:select />
allowCopy={false}
json={flow_input}
on:select={(e) => dispatch('select', `flow_input.${e.detail}`)}
/>
</div> </div>
{/if} {/if}
</div> </div>

View File

@@ -171,13 +171,36 @@ export function validatePassword(password: string): boolean {
const portalDivs = ['app-editor-select'] const portalDivs = ['app-editor-select']
export function clickOutside(node: Node, capture?: boolean): { destroy(): void } { interface ClickOutsideOptions {
const handleClick = (event: MouseEvent) => { capture?: boolean
exclude?: (() => Promise<HTMLElement[]>) | HTMLElement[] | undefined
}
export function clickOutside(
node: Node,
options?: ClickOutsideOptions | boolean
): { destroy(): void; update(newOptions: ClickOutsideOptions | boolean): void } {
const handleClick = async (event: MouseEvent) => {
const target = event.target as HTMLElement const target = event.target as HTMLElement
const opts = typeof options === 'boolean' ? { capture: options } : options
if (node && !node.contains(target) && !event.defaultPrevented) { let excludedElements: HTMLElement[] = []
if (opts?.exclude) {
if (Array.isArray(opts.exclude)) {
excludedElements = opts.exclude
} else {
excludedElements = await opts.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 portalDivsSelector = portalDivs.map((id) => `#${id}`).join(', ')
const parent = target.closest(portalDivsSelector) const parent = target.closest(portalDivsSelector)
if (!parent) { if (!parent) {
@@ -186,11 +209,15 @@ export function clickOutside(node: Node, capture?: boolean): { destroy(): void }
} }
} }
document.addEventListener('click', handleClick, capture ?? true) const capture = typeof options === 'boolean' ? options : options?.capture ?? true
document.addEventListener('click', handleClick, capture)
return { return {
update(newOptions: ClickOutsideOptions | boolean) {
options = newOptions
},
destroy() { destroy() {
document.removeEventListener('click', handleClick, capture ?? true) document.removeEventListener('click', handleClick, capture)
} }
} }
} }
@@ -724,7 +751,7 @@ export async function tryEvery({
try { try {
await tryCode() await tryCode()
break break
} catch (err) { } } catch (err) {}
i++ i++
} }
if (i >= times) { if (i >= times) {
@@ -948,5 +975,4 @@ export function getSchemaFromProperties(properties: { [name: string]: SchemaProp
export function validateFileExtension(ext: string) { export function validateFileExtension(ext: string) {
const validExtensionRegex = /^[a-zA-Z0-9]+([._][a-zA-Z0-9]+)*$/ const validExtensionRegex = /^[a-zA-Z0-9]+([._][a-zA-Z0-9]+)*$/
return validExtensionRegex.test(ext) return validExtensionRegex.test(ext)
} }