Properly propagate dynamic configuration inside dropdown widget (#8983)

Closes #8932

Now we use a bit more robust mechanism for passing dynamic configuration down the widget tree inside dropdowns, no longer relying on the `label`s used for dropdown items.

Curiously, we still need to use a hotfix implemented earlier, as we won’t have info about the currently selected item otherwise. Highlight for the currently selected item is not crucial as proper dynamic config, so we can leave with the current solution in the meantime.

No visual changes to the IDE, apart from fixed highlight for currently selected item.

# Important Notes
Target branch: #8950, for easier testing.
This commit is contained in:
Ilya Bogdanov 2024-02-12 14:17:29 +04:00 committed by GitHub
parent 129022ae12
commit e1943bdd49
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 58 additions and 17 deletions

View File

@ -9,7 +9,7 @@ class DropDownLocator {
constructor(page: Page) {
this.dropDown = page.locator('.dropdownContainer')
this.items = this.dropDown.locator('.selectable-item')
this.items = this.dropDown.locator('.selectable-item, .selected-item')
}
async expectVisibleWithOptions(page: Page, options: string[]): Promise<void> {

View File

@ -14,6 +14,7 @@ import {
} from '@/providers/widgetRegistry/configuration'
import { useGraphStore } from '@/stores/graph'
import { useProjectStore, type NodeVisualizationConfiguration } from '@/stores/project'
import { entryQn } from '@/stores/suggestionDatabase/entry'
import { assert, assertUnreachable } from '@/util/assert'
import { Ast } from '@/util/ast'
import {
@ -121,7 +122,7 @@ const visualizationConfig = computed<Opt<NodeVisualizationConfiguration>>(() =>
const expressionId = selfArgumentExternalId.value
const astId = props.input.value.id
if (astId == null || expressionId == null) return null
const info = graph.db.getMethodCallInfo(astId)
const info = methodCallInfo.value
if (!info) return null
const args = info.suggestion.annotations
if (args.length === 0) return null
@ -141,6 +142,12 @@ const visualizationConfig = computed<Opt<NodeVisualizationConfiguration>>(() =>
const visualizationData = project.useVisualizationData(visualizationConfig)
const widgetConfiguration = computed(() => {
if (props.input.dynamicConfig?.kind === 'FunctionCall') return props.input.dynamicConfig
if (props.input.dynamicConfig?.kind === 'OneOfFunctionCalls' && methodCallInfo.value != null) {
const cfg = props.input.dynamicConfig
const info = methodCallInfo.value
const name = entryQn(info?.suggestion)
return cfg.possibleFunctions.get(name)
}
const data = visualizationData.value
if (data?.ok) {
const parseResult = argsWidgetConfigurationSchema.safeParse(data.value)

View File

@ -4,7 +4,7 @@ import SvgIcon from '@/components/SvgIcon.vue'
import DropdownWidget from '@/components/widgets/DropdownWidget.vue'
import { Score, WidgetInput, defineWidget, widgetProps } from '@/providers/widgetRegistry'
import {
functionCallConfiguration,
singleChoiceConfiguration,
type ArgumentWidgetConfiguration,
} from '@/providers/widgetRegistry/configuration'
import { useGraphStore } from '@/stores/graph'
@ -84,6 +84,11 @@ const tagLabels = computed(() => tags.value.map((tag) => tag.label ?? tag.expres
const removeSurroundingParens = (expr?: string) => expr?.trim().replaceAll(/(^[(])|([)]$)/g, '')
const selectedIndex = ref<number>()
// When the input changes, we need to reset the selected index.
watch(
() => props.input.value,
() => (selectedIndex.value = undefined),
)
const selectedTag = computed(() => {
if (selectedIndex.value != null) {
return tags.value[selectedIndex.value]
@ -100,16 +105,14 @@ const selectedTag = computed(() => {
}
})
const selectedExpression = computed(() => {
if (selectedTag.value == null) return WidgetInput.valueRepr(props.input)
return selectedTag.value.expression
const selectedLabel = computed(() => {
return selectedTag.value?.label
})
const innerWidgetInput = computed(() => {
if (selectedTag.value == null) return props.input
const parameters = selectedTag.value.parameters
if (!parameters) return props.input
const config = functionCallConfiguration(parameters)
return { ...props.input, dynamicConfig: config }
if (props.input.dynamicConfig == null) return props.input
const config = props.input.dynamicConfig
if (config.kind !== 'Single_Choice') return props.input
return { ...props.input, dynamicConfig: singleChoiceConfiguration(config) }
})
const showDropdownWidget = ref(false)
@ -117,6 +120,11 @@ function toggleDropdownWidget() {
showDropdownWidget.value = !showDropdownWidget.value
}
function onClick(index: number) {
selectedIndex.value = index
showDropdownWidget.value = false
}
// When the selected index changes, we update the expression content.
watch(selectedIndex, (_index) => {
let edit: Ast.MutableModule | undefined
@ -127,11 +135,10 @@ watch(selectedIndex, (_index) => {
props.onUpdate({
edit,
portUpdate: {
value: selectedExpression.value,
value: selectedTag.value?.expression,
origin: asNot<TokenId>(props.input.portId),
},
})
showDropdownWidget.value = false
})
</script>
@ -155,9 +162,9 @@ export const widgetDefinition = defineWidget(WidgetInput.isAstOrPlaceholder, {
class="dropdownContainer"
:color="'var(--node-color-primary)'"
:values="tagLabels"
:selectedValue="selectedExpression"
:selectedValue="selectedLabel"
@pointerdown.stop
@click="selectedIndex = $event"
@click="onClick($event)"
/>
</div>
</template>

View File

@ -51,7 +51,9 @@ const NEXT_SORT_DIRECTION: Record<SortDirection, SortDirection> = {
<ul class="list" :style="{ background: color }" @wheel.stop>
<template v-for="[value, index] in sortedValuesAndIndices" :key="value">
<li v-if="value === selectedValue">
<div class="selected-item"><span v-text="value"></span></div>
<div class="selected-item button" @pointerdown="emit('click', index)">
<span v-text="value"></span>
</div>
</li>
<li v-else class="selectable-item button" @pointerdown="emit('click', index)">
<span v-text="value"></span>

View File

@ -133,7 +133,7 @@ export interface WidgetProps<T> {
/**
* Information about widget update.
*
* When widget want's to change its value, it should emit this with `portUpdate` set (as their
* When widget wants to change its value, it should emit this with `portUpdate` set (as their
* port may not represent any existing AST node) with `edit` containing any additional modifications
* (like inserting necessary imports).
*

View File

@ -67,6 +67,7 @@ export type WidgetConfiguration =
| FolderBrowse
| FileBrowse
| FunctionCall
| OneOfFunctionCalls
export interface VectorEditor {
kind: 'Vector_Editor'
@ -110,11 +111,25 @@ export interface SingleChoice {
values: Choice[]
}
/** Dynamic configuration for a function call with a list of arguments with known dynamic configuration.
* This kind of config is not provided by the engine directly, but is derived from other config types by widgets. */
export interface FunctionCall {
kind: 'FunctionCall'
parameters: Map<string, (WidgetConfiguration & WithDisplay) | null>
}
/** Dynamic configuration for one of the possible function calls. It is typically the case for dropdown widget.
* One of function calls will be chosen by WidgetFunction basing on the actual AST at the call site,
* and the configuration will be used in child widgets.
* This kind of config is not provided by the engine directly, but is derived from other config types by widgets. */
export interface OneOfFunctionCalls {
kind: 'OneOfFunctionCalls'
/** A list of possible function calls and their corresponding configuration.
* The key is typically a fully qualified name of the function, but in general it can be anything,
* depending on the widget implementation. */
possibleFunctions: Map<string, FunctionCall>
}
export const widgetConfigurationSchema: z.ZodType<
WidgetConfiguration & WithDisplay,
z.ZodTypeDef,
@ -166,3 +181,13 @@ export function functionCallConfiguration(parameters: ArgumentWidgetConfiguratio
parameters: new Map(parameters),
}
}
/** A configuration for the inner widget of the dropdown widget. */
export function singleChoiceConfiguration(config: SingleChoice): OneOfFunctionCalls {
return {
kind: 'OneOfFunctionCalls',
possibleFunctions: new Map(
config.values.map((value) => [value.value, functionCallConfiguration(value.parameters)]),
),
}
}