mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-04 16:03:06 +00:00
Add auto filtering in prop-picker
This commit is contained in:
@@ -25,6 +25,7 @@
|
||||
import type { FlowCopilotContext } from './copilot/flow'
|
||||
import StepInputGen from './copilot/StepInputGen.svelte'
|
||||
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 arg: InputTransform | any
|
||||
@@ -59,6 +60,8 @@
|
||||
const { shouldUpdatePropertyType, exprsToSet } =
|
||||
getContext<FlowCopilotContext | undefined>('FlowCopilotContext') || {}
|
||||
|
||||
const { inputMatches } = getContext<PropPickerWrapperContext>('PropPickerWrapper')
|
||||
|
||||
function setExpr() {
|
||||
const newArg = $exprsToSet?.[argName]
|
||||
if (newArg) {
|
||||
@@ -135,16 +138,19 @@
|
||||
|
||||
let codeInjectionDetected = false
|
||||
|
||||
const dynamicTemplateRegexPairs = buildPrefixRegex([
|
||||
'flow_input',
|
||||
'results',
|
||||
'resource',
|
||||
'variable'
|
||||
])
|
||||
|
||||
function checkCodeInjection(rawValue: string) {
|
||||
if (!arg) {
|
||||
return
|
||||
if (!arg || !rawValue || rawValue.length < 3 || !dynamicTemplateRegexPairs) {
|
||||
return []
|
||||
}
|
||||
|
||||
const dynamicTemplateRegex = new RegExp(
|
||||
/^(flow_input\.|results\.|flo$|flow$|flow_$|flow_i$|flow_in$|flow_inp$|flow_inpu$|flow_input$|res$|resu$|resul$|result$|results$).*/
|
||||
)
|
||||
|
||||
codeInjectionDetected = dynamicTemplateRegex.test(rawValue)
|
||||
const matches = dynamicTemplateRegexPairs.filter(({ regex }) => regex.test(rawValue))
|
||||
return matches.map((m) => ({ word: m.word, value: rawValue }))
|
||||
}
|
||||
|
||||
async function setJavaScriptExpr(rawValue: string) {
|
||||
@@ -210,9 +216,24 @@
|
||||
|
||||
const { focusProp, propPickerConfig } = getContext<PropPickerWrapperContext>('PropPickerWrapper')
|
||||
|
||||
$: isStaticTemplate(inputCat) && propertyType == 'static' && setPropertyType(arg?.value)
|
||||
$: updateStaticInput(inputCat, propertyType, arg)
|
||||
|
||||
$: isStaticTemplate(inputCat) && propertyType == 'static' && checkCodeInjection(arg?.value)
|
||||
function updateStaticInput(
|
||||
inputCat: InputCat,
|
||||
propertyType: 'static' | 'javascript',
|
||||
arg: InputTransform | any
|
||||
) {
|
||||
if (!isStaticTemplate(inputCat)) {
|
||||
return
|
||||
}
|
||||
if (propertyType == 'static') {
|
||||
setPropertyType(arg?.value)
|
||||
codeInjectionDetected = checkCodeInjection(arg?.value).length > 0
|
||||
} else if (propertyType == 'javascript' && focused) {
|
||||
setPropertyType(arg?.expr)
|
||||
$inputMatches = checkCodeInjection(arg?.expr)
|
||||
}
|
||||
}
|
||||
|
||||
function setDefaultCode() {
|
||||
if (!arg?.value) {
|
||||
@@ -446,8 +467,8 @@
|
||||
on:click={() => setJavaScriptExpr(arg.value)}
|
||||
>
|
||||
<span class="font-normal"
|
||||
>Javascript expression detected - press
|
||||
<span class="font-bold">TAB</span> to switch
|
||||
>JavaScript expression detected - press
|
||||
<span class="font-bold">TAB</span> to exit static mode
|
||||
</span>
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
const propPickerConfig = writable<PropPickerConfig | undefined>(undefined)
|
||||
setContext<PropPickerWrapperContext>('PropPickerWrapper', {
|
||||
propPickerConfig,
|
||||
inputMatches: writable(undefined),
|
||||
focusProp: (propName, insertionMode, onSelect) => {
|
||||
propPickerConfig.set({
|
||||
propName,
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
setContext<PropPickerWrapperContext>('PropPickerWrapper', {
|
||||
focusProp: () => {},
|
||||
propPickerConfig: writable(undefined),
|
||||
inputMatches: writable(undefined),
|
||||
clearFocus: () => {},
|
||||
filteredPickableProperties: writable(undefined)
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ export type PropPickerConfig = {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import { keepByKey } from './utils'
|
||||
import type { PickableProperties } from '../flows/previousResults'
|
||||
import ClearableInput from '../common/clearableInput/ClearableInput.svelte'
|
||||
import { filterNestedObject } from '../flows/previousResults'
|
||||
|
||||
export let pickableProperties: PickableProperties
|
||||
export let displayContext = true
|
||||
@@ -22,37 +23,30 @@
|
||||
let displayVariable = false
|
||||
let displayResources = false
|
||||
let allResultsCollapsed = true
|
||||
let flowInputsFiltered: Record<string, any> = {}
|
||||
let resultByIdFiltered: Record<string, any> = {}
|
||||
let collapsableInitialState:
|
||||
| {
|
||||
allResultsCollapsed: boolean
|
||||
displayVariable: boolean
|
||||
displayResources: boolean
|
||||
}
|
||||
| undefined
|
||||
|
||||
const EMPTY_STRING = ''
|
||||
let search = ''
|
||||
|
||||
const { propPickerConfig, filteredPickableProperties } =
|
||||
const { propPickerConfig, filteredPickableProperties, inputMatches } =
|
||||
getContext<PropPickerWrapperContext>('PropPickerWrapper')
|
||||
|
||||
$filteredPickableProperties = { ...pickableProperties }
|
||||
|
||||
$: flowInputsFiltered =
|
||||
search === EMPTY_STRING
|
||||
? pickableProperties.flow_input
|
||||
: keepByKey(pickableProperties.flow_input, search)
|
||||
|
||||
$: resultByIdFiltered =
|
||||
search === EMPTY_STRING
|
||||
? pickableProperties.priorIds
|
||||
: keepByKey(pickableProperties.priorIds, search)
|
||||
$: filterPickableProperties(), updateCollapsable(), search, $inputMatches
|
||||
|
||||
$: suggestedPropsFiltered = $propPickerConfig
|
||||
? keepByKey(pickableProperties.priorIds, $propPickerConfig.propName)
|
||||
: undefined
|
||||
|
||||
$: resultByIdFiltered &&
|
||||
$filteredPickableProperties &&
|
||||
($filteredPickableProperties.priorIds = resultByIdFiltered)
|
||||
|
||||
$: flowInputsFiltered &&
|
||||
$filteredPickableProperties &&
|
||||
($filteredPickableProperties.flow_input = flowInputsFiltered)
|
||||
|
||||
async function loadVariables() {
|
||||
variables = Object.fromEntries(
|
||||
(
|
||||
@@ -72,6 +66,75 @@
|
||||
).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>
|
||||
|
||||
<div class="flex flex-col h-full !bg-surface rounded overflow-hidden">
|
||||
@@ -94,19 +157,21 @@
|
||||
class="overflow-y-auto px-2 pt-2 grow"
|
||||
class:bg-surface-secondary={!$propPickerConfig && !notSelectable}
|
||||
>
|
||||
<div class="flex justify-between items-center space-x-1">
|
||||
<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}
|
||||
pureViewer={!$propPickerConfig}
|
||||
json={flowInputsFiltered}
|
||||
prefix="flow_input"
|
||||
on:select
|
||||
/>
|
||||
</div>
|
||||
{#if flowInputsFiltered && Object.keys(flowInputsFiltered).length > 0}
|
||||
<div class="flex justify-between items-center space-x-1">
|
||||
<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}
|
||||
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">
|
||||
@@ -151,15 +216,16 @@
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
{#if previousId}
|
||||
{@const json = Object.fromEntries(
|
||||
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">
|
||||
<ObjectViewer
|
||||
{allowCopy}
|
||||
pureViewer={!$propPickerConfig}
|
||||
json={Object.fromEntries(
|
||||
Object.entries(resultByIdFiltered).filter(([k, v]) => k == previousId)
|
||||
)}
|
||||
{json}
|
||||
prefix="results"
|
||||
on:select
|
||||
/>
|
||||
@@ -194,112 +260,117 @@
|
||||
/>
|
||||
</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>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if displayContext}
|
||||
{#if !$inputMatches || $inputMatches.some((match) => match.word === 'variable')}
|
||||
<div class="overflow-y-auto mb-2">
|
||||
<span class="font-normal text-sm text-tertiary">All Results :</span>
|
||||
{#if !allResultsCollapsed}
|
||||
<span class="font-normal text-sm text-secondary">Variables :</span>
|
||||
|
||||
{#if displayVariable}
|
||||
<Button
|
||||
color="light"
|
||||
size="xs2"
|
||||
variant="border"
|
||||
on:click={() => {
|
||||
allResultsCollapsed = true
|
||||
displayVariable = false
|
||||
}}
|
||||
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
|
||||
/>
|
||||
<ObjectViewer
|
||||
{allowCopy}
|
||||
pureViewer={!$propPickerConfig}
|
||||
rawKey={true}
|
||||
json={variables}
|
||||
prefix="variable"
|
||||
on:select
|
||||
/>
|
||||
{:else}
|
||||
<Button
|
||||
color="light"
|
||||
size="xs2"
|
||||
variant="border"
|
||||
on:click={async () => {
|
||||
await loadVariables()
|
||||
displayVariable = true
|
||||
}}
|
||||
wrapperClasses="inline-flex w-fit h-5"
|
||||
btnClasses="font-semibold border-nord-300 rounded-[0.275rem] p-1"
|
||||
>
|
||||
{'{...}'}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if !$inputMatches || $inputMatches.some((match) => match.word === 'resource')}
|
||||
<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"
|
||||
on:click={() => {
|
||||
displayResources = false
|
||||
}}
|
||||
wrapperClasses="inline-flex w-fit h-5"
|
||||
btnClasses="font-semibold text-primary border-nord-300 rounded-[0.275rem]">-</Button
|
||||
>
|
||||
<ObjectViewer
|
||||
{allowCopy}
|
||||
pureViewer={!$propPickerConfig}
|
||||
rawKey={true}
|
||||
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 displayContext}
|
||||
<div class="overflow-y-auto mb-2">
|
||||
<span class="font-normal text-sm text-secondary">Variables :</span>
|
||||
|
||||
{#if displayVariable}
|
||||
<Button
|
||||
color="light"
|
||||
size="xs2"
|
||||
variant="border"
|
||||
on:click={() => {
|
||||
displayVariable = false
|
||||
}}
|
||||
wrapperClasses="inline-flex w-fit h-4"
|
||||
btnClasses="font-normal text-primary border-nord-300 rounded-[0.275rem]">-</Button
|
||||
>
|
||||
|
||||
<ObjectViewer
|
||||
{allowCopy}
|
||||
pureViewer={!$propPickerConfig}
|
||||
rawKey={true}
|
||||
json={variables}
|
||||
prefix="variable"
|
||||
on:select
|
||||
/>
|
||||
{:else}
|
||||
<Button
|
||||
color="light"
|
||||
size="xs2"
|
||||
variant="border"
|
||||
on:click={async () => {
|
||||
await loadVariables()
|
||||
displayVariable = true
|
||||
}}
|
||||
wrapperClasses="inline-flex w-fit h-5"
|
||||
btnClasses="font-semibold border-nord-300 rounded-[0.275rem] p-1"
|
||||
>
|
||||
{'{...}'}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<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"
|
||||
on:click={() => {
|
||||
displayResources = false
|
||||
}}
|
||||
wrapperClasses="inline-flex w-fit h-5"
|
||||
btnClasses="font-semibold text-primary border-nord-300 rounded-[0.275rem]">-</Button
|
||||
>
|
||||
<ObjectViewer
|
||||
{allowCopy}
|
||||
pureViewer={!$propPickerConfig}
|
||||
rawKey={true}
|
||||
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}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user