feat: Workflow form nodes support reference assignment (#3866) #2439

This commit is contained in:
shaohuzhang1 2025-08-15 18:08:18 +08:00 committed by GitHub
parent f78027818d
commit a85c36f289
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 301 additions and 75 deletions

View File

@ -17,6 +17,19 @@ from application.flow.i_step_node import NodeResult
from application.flow.step_node.form_node.i_form_node import IFormNode
def get_default_option(option_list, _type, value_field):
if option_list is not None and len(option_list) > 0:
default_value_list = [o.get(value_field) for o in option_list if o.get('default')]
if len(default_value_list) == 0:
return option_list[0].get(value_field)
else:
if _type == 'MultiSelect':
return default_value_list
else:
return default_value_list[0]
return []
def write_context(step_variable: Dict, global_variable: Dict, node, workflow):
if step_variable is not None:
for key in step_variable:
@ -44,6 +57,28 @@ class BaseFormNode(IFormNode):
for key in form_data:
self.context[key] = form_data[key]
def reset_field(self, field):
if ['SingleSelect', 'MultiSelect', 'RadioCard'].__contains__(field.get('input_type')):
if field.get('assignment_method') == 'ref_variables':
option_list = self.workflow_manage.get_reference_field(field.get('option_list')[0],
field.get('option_list')[1:])
field['option_list'] = option_list
field['default_value'] = get_default_option(option_list, field.get('input_type'),
field.get('value_field'))
reset_field = ['field', 'label', 'default_value']
for f in reset_field:
_value = field[f]
if isinstance(_value, str):
field[f] = self.workflow_manage.generate_prompt(_value)
else:
_label_value = _value.get('label')
_value['label'] = self.workflow_manage.generate_prompt(_label_value)
tooltip = _value.get('attrs').get('tooltip')
if tooltip is not None:
_value.get('attrs')['tooltip'] = self.workflow_manage.generate_prompt(tooltip)
return field
def execute(self, form_field_list, form_content_format, form_data, **kwargs) -> NodeResult:
if form_data is not None:
self.context['is_submit'] = True
@ -52,6 +87,7 @@ class BaseFormNode(IFormNode):
self.context[key] = form_data.get(key)
else:
self.context['is_submit'] = False
form_field_list = [self.reset_field(field) for field in form_field_list]
form_setting = {"form_field_list": form_field_list, "runtime_node_id": self.runtime_node_id,
"chat_record_id": self.flow_params_serializer.data.get("chat_record_id"),
"is_submit": self.context.get("is_submit", False)}
@ -60,6 +96,7 @@ class BaseFormNode(IFormNode):
form_content_format = self.workflow_manage.reset_prompt(form_content_format)
prompt_template = PromptTemplate.from_template(form_content_format, template_format='jinja2')
value = prompt_template.format(form=form, context=context)
return NodeResult(
{'result': value, 'form_field_list': form_field_list, 'form_content_format': form_content_format}, {},
_write_context=write_context)

View File

@ -1,5 +1,28 @@
<template>
<el-form-item>
<el-form-item v-if="getModel">
<template #label>
<div class="flex-between">
{{ $t('dynamicsForm.AssignmentMethod.label', '赋值方式') }}
</div>
</template>
<el-row style="width: 100%" :gutter="10">
<el-radio-group v-model="formValue.assignment_method">
<el-radio :value="item.value" size="large" v-for="item in assignment_method_option_list">{{
item.label
}}</el-radio>
</el-radio-group>
</el-row>
</el-form-item>
<NodeCascader
v-if="formValue.assignment_method == 'ref_variables'"
ref="nodeCascaderRef"
:nodeModel="model"
class="w-full"
:placeholder="$t('views.applicationWorkflow.variable.placeholder')"
v-model="formValue.option_list"
/>
<el-form-item v-if="formValue.assignment_method == 'custom'">
<template #label>
<div class="flex-between">
{{ $t('dynamicsForm.Select.label') }}
@ -51,6 +74,7 @@
</el-row>
</el-form-item>
<el-form-item
v-if="formValue.assignment_method == 'custom'"
class="defaultValueItem"
:label="$t('dynamicsForm.default.label')"
:required="formValue.required"
@ -92,8 +116,34 @@
</el-form-item>
</template>
<script setup lang="ts">
import { computed, onMounted } from 'vue'
import { computed, onMounted, inject } from 'vue'
import NodeCascader from '@/workflow/common/NodeCascader.vue'
import { t } from '@/locales'
const getModel = inject('getModel') as any
const assignment_method_option_list = computed(() => {
const option_list = [
{
label: t('dynamicsForm.AssignmentMethod.custom.label', '自定义'),
value: 'custom',
},
]
if (getModel) {
option_list.push({
label: t('dynamicsForm.AssignmentMethod.ref_variables.label', '引用变量'),
value: 'ref_variables',
})
}
return option_list
})
const model = computed(() => {
if (getModel) {
return getModel()
} else {
return null
}
})
const props = defineProps<{
modelValue: any
}>()
@ -128,17 +178,20 @@ const getData = () => {
text_field: 'label',
value_field: 'value',
option_list: formValue.value.option_list,
assignment_method: formValue.value.assignment_method || 'custom',
}
}
const rander = (form_data: any) => {
formValue.value.option_list = form_data.option_list || []
formValue.value.default_value = form_data.default_value
formValue.value.assignment_method = form_data.assignment_method || 'custom'
}
defineExpose({ getData, rander })
onMounted(() => {
formValue.value.option_list = []
formValue.value.default_value = ''
formValue.value.assignment_method = 'custom'
if (formValue.value.show_default_value === undefined) {
formValue.value.show_default_value = true
}

View File

@ -1,5 +1,28 @@
<template>
<el-form-item>
<el-form-item v-if="getModel">
<template #label>
<div class="flex-between">
{{ $t('dynamicsForm.AssignmentMethod.label', '赋值方式') }}
</div>
</template>
<el-row style="width: 100%" :gutter="10">
<el-radio-group v-model="formValue.assignment_method">
<el-radio :value="item.value" size="large" v-for="item in assignment_method_option_list">{{
item.label
}}</el-radio>
</el-radio-group>
</el-row>
</el-form-item>
<NodeCascader
v-if="formValue.assignment_method == 'ref_variables'"
ref="nodeCascaderRef"
:nodeModel="model"
class="w-full"
:placeholder="$t('views.applicationWorkflow.variable.placeholder')"
v-model="formValue.option_list"
/>
<el-form-item v-if="formValue.assignment_method === 'custom'">
<template #label>
<div class="flex-between">
{{ $t('dynamicsForm.Select.label') }}
@ -51,7 +74,9 @@
</el-col>
</el-row>
</el-form-item>
<el-form-item
v-if="formValue.assignment_method === 'custom'"
class="defaultValueItem"
:label="$t('dynamicsForm.default.label')"
:required="formValue.required"
@ -83,8 +108,35 @@
</el-form-item>
</template>
<script setup lang="ts">
import { computed, onMounted } from 'vue'
import { computed, onMounted, inject, ref } from 'vue'
import RadioCard from '@/components/dynamics-form/items/radio/RadioCard.vue'
import NodeCascader from '@/workflow/common/NodeCascader.vue'
import { t } from '@/locales'
const getModel = inject('getModel') as any
const assignment_method_option_list = computed(() => {
const option_list = [
{
label: t('dynamicsForm.AssignmentMethod.custom.label', '自定义'),
value: 'custom',
},
]
if (getModel) {
option_list.push({
label: t('dynamicsForm.AssignmentMethod.ref_variables.label', '引用变量'),
value: 'ref_variables',
})
}
return option_list
})
const model = computed(() => {
if (getModel) {
return getModel()
} else {
return null
}
})
const props = defineProps<{
modelValue: any
}>()
@ -121,17 +173,20 @@ const getData = () => {
text_field: 'label',
value_field: 'value',
option_list: formValue.value.option_list,
assignment_method: formValue.value.assignment_method || 'custom',
}
}
const rander = (form_data: any) => {
formValue.value.option_list = form_data.option_list || []
formValue.value.default_value = form_data.default_value
formValue.value.assignment_method = form_data.assignment_method || 'custom'
}
defineExpose({ getData, rander })
onMounted(() => {
formValue.value.option_list = []
formValue.value.default_value = ''
formValue.value.assignment_method = 'custom'
if (formValue.value.show_default_value === undefined) {
formValue.value.show_default_value = true
}

View File

@ -1,5 +1,28 @@
<template>
<el-form-item>
<el-form-item v-if="getModel">
<template #label>
<div class="flex-between">
{{ $t('dynamicsForm.AssignmentMethod.label', '赋值方式') }}
</div>
</template>
<el-row style="width: 100%" :gutter="10">
<el-radio-group v-model="formValue.assignment_method">
<el-radio :value="item.value" size="large" v-for="item in assignment_method_option_list">{{
item.label
}}</el-radio>
</el-radio-group>
</el-row>
</el-form-item>
<NodeCascader
v-if="formValue.assignment_method == 'ref_variables'"
ref="nodeCascaderRef"
:nodeModel="model"
class="w-full"
:placeholder="$t('views.applicationWorkflow.variable.placeholder')"
v-model="formValue.option_list"
/>
<el-form-item v-if="formValue.assignment_method == 'custom'">
<template #label>
<div class="flex-between">
{{ $t('dynamicsForm.Select.label') }}
@ -52,6 +75,7 @@
</el-row>
</el-form-item>
<el-form-item
v-if="formValue.assignment_method == 'custom'"
class="defaultValueItem"
:required="formValue.required"
prop="default_value"
@ -85,8 +109,34 @@
</el-form-item>
</template>
<script setup lang="ts">
import { computed, onMounted } from 'vue'
import { computed, onMounted, inject } from 'vue'
import NodeCascader from '@/workflow/common/NodeCascader.vue'
import { t } from '@/locales'
const getModel = inject('getModel') as any
const assignment_method_option_list = computed(() => {
const option_list = [
{
label: t('dynamicsForm.AssignmentMethod.custom.label', '自定义'),
value: 'custom',
},
]
if (getModel) {
option_list.push({
label: t('dynamicsForm.AssignmentMethod.ref_variables.label', '引用变量'),
value: 'ref_variables',
})
}
return option_list
})
const model = computed(() => {
if (getModel) {
return getModel()
} else {
return null
}
})
const props = defineProps<{
modelValue: any
}>()
@ -121,18 +171,21 @@ const getData = () => {
text_field: 'label',
value_field: 'value',
option_list: formValue.value.option_list,
assignment_method: formValue.value.assignment_method || 'custom',
}
}
const rander = (form_data: any) => {
formValue.value.option_list = form_data.option_list || []
formValue.value.default_value = form_data.default_value
formValue.value.show_default_value = form_data.show_default_value
formValue.value.assignment_method = form_data.assignment_method || 'custom'
}
defineExpose({ getData, rander })
onMounted(() => {
formValue.value.option_list = []
formValue.value.default_value = ''
formValue.value.assignment_method = 'custom'
if (formValue.value.show_default_value === undefined) {
formValue.value.show_default_value = true
}

View File

@ -10,11 +10,11 @@
style="--el-card-padding: 12px 16px"
:class="[
inputDisabled ? 'is-disabled' : '',
modelValue == item[valueField] ? 'active' : ''
modelValue == item[valueField] ? 'active' : '',
]"
@click="inputDisabled ? () => {} : selected(item[valueField])"
:innerHTML="item[textField]"
>
{{ item[textField] }}
</el-card>
</el-col>
</template>

View File

@ -9,46 +9,46 @@ export default {
DatePicker: 'Date Picker',
JsonInput: 'JSON',
RadioCard: 'Radio Card',
RadioRow: 'Radio Row'
RadioRow: 'Radio Row',
},
default: {
label: 'Default',
placeholder: 'Please enter a default',
requiredMessage: ' is a required property',
show: 'Show Default'
show: 'Show Default',
},
tip: {
requiredMessage: 'cannot be empty',
jsonMessage: 'Incorrect JSON format'
jsonMessage: 'Incorrect JSON format',
},
searchBar: {
placeholder: 'Please enter keywords to search'
placeholder: 'Please enter keywords to search',
},
paramForm: {
field: {
label: 'Parameter',
placeholder: 'Please enter a parameter',
requiredMessage: 'Parameter is a required property',
requiredMessage2: 'Only letters, numbers, and underscores are allowed'
requiredMessage2: 'Only letters, numbers, and underscores are allowed',
},
name: {
label: 'Name',
placeholder: 'Please enter a name',
requiredMessage: 'Name is a required property'
requiredMessage: 'Name is a required property',
},
tooltip: {
label: 'Tooltip',
placeholder: 'Please enter a tooltip'
placeholder: 'Please enter a tooltip',
},
required: {
label: 'Required',
requiredMessage: 'Required is a required property'
requiredMessage: 'Required is a required property',
},
input_type: {
label: 'Type',
placeholder: 'Please select a type',
requiredMessage: 'Type is a required property'
}
requiredMessage: 'Type is a required property',
},
},
DatePicker: {
placeholder: 'Select Date',
@ -58,35 +58,35 @@ export default {
datetime: 'Date Time',
dataType: {
label: 'Date Type',
placeholder: 'Please select a date type'
placeholder: 'Please select a date type',
},
format: {
label: 'Format',
placeholder: 'Please select a format'
}
placeholder: 'Please select a format',
},
},
Select: {
label: 'Option Value',
placeholder: 'Please enter an option value'
placeholder: 'Please enter an option value',
},
tag: {
label: 'Tag',
placeholder: 'Please enter an option label'
placeholder: 'Please enter an option label',
},
Slider: {
showInput: {
label: 'Show Input Box'
label: 'Show Input Box',
},
valueRange: {
label: 'Value Range',
minRequired: 'Minimum value is required',
maxRequired: 'Maximum value is required'
maxRequired: 'Maximum value is required',
},
step: {
label: 'Step Value',
requiredMessage1: 'Step value is required',
requiredMessage2: 'Step value cannot be 0'
}
requiredMessage2: 'Step value cannot be 0',
},
},
TextInput: {
length: {
@ -96,7 +96,16 @@ export default {
requiredMessage1: 'Length must be between',
requiredMessage2: 'and',
requiredMessage3: 'characters',
requiredMessage4: 'Text length is a required parameter'
}
}
requiredMessage4: 'Text length is a required parameter',
},
},
AssignmentMethod: {
label: 'Assignment Method',
custom: {
label: 'Custom',
},
ref_variables: {
label: 'Reference Variables',
},
},
}

View File

@ -9,46 +9,46 @@ export default {
DatePicker: '日期',
JsonInput: 'JSON文本框',
RadioCard: '选项卡',
RadioRow: '单行选项卡'
RadioRow: '单行选项卡',
},
default: {
label: '默认值',
placeholder: '请输入默认值',
requiredMessage: '为必填属性',
show: '显示默认值'
show: '显示默认值',
},
tip: {
requiredMessage: '不能为空',
jsonMessage: 'JSON格式不正确'
jsonMessage: 'JSON格式不正确',
},
searchBar: {
placeholder: '请输入关键字搜索'
placeholder: '请输入关键字搜索',
},
paramForm: {
field: {
label: '参数',
placeholder: '请输入参数',
requiredMessage: '参数 为必填属性',
requiredMessage2: '只能输入字母数字和下划线'
requiredMessage2: '只能输入字母数字和下划线',
},
name: {
label: '显示名称',
placeholder: '请输入显示名称',
requiredMessage: '显示名称 为必填属性'
requiredMessage: '显示名称 为必填属性',
},
tooltip: {
label: '参数提示说明',
placeholder: '请输入参数提示说明'
placeholder: '请输入参数提示说明',
},
required: {
label: '是否必填',
requiredMessage: '是否必填 为必填属性'
requiredMessage: '是否必填 为必填属性',
},
input_type: {
label: '组件类型',
placeholder: '请选择组件类型',
requiredMessage: '组建类型 为必填属性'
}
requiredMessage: '组建类型 为必填属性',
},
},
DatePicker: {
placeholder: '选择日期',
@ -58,35 +58,35 @@ export default {
datetime: '日期时间',
dataType: {
label: '时间类型',
placeholder: '请选择时间类型'
placeholder: '请选择时间类型',
},
format: {
label: '格式',
placeholder: '请选择格式'
}
placeholder: '请选择格式',
},
},
Select: {
label: '选项值',
placeholder: '请输入选项值'
placeholder: '请输入选项值',
},
tag: {
label: '标签',
placeholder: '请输入选项标签'
placeholder: '请输入选项标签',
},
Slider: {
showInput: {
label: '是否带输入框'
label: '是否带输入框',
},
valueRange: {
label: '取值范围',
minRequired: '最小值必填',
maxRequired: '最大值必填'
maxRequired: '最大值必填',
},
step: {
label: '步长值',
requiredMessage1: '步长值必填',
requiredMessage2: '步长不能为0'
}
requiredMessage2: '步长不能为0',
},
},
TextInput: {
length: {
@ -96,7 +96,16 @@ export default {
requiredMessage1: '长度在',
requiredMessage2: '到',
requiredMessage3: '个字符',
requiredMessage4: '文本长度为必填参数'
}
}
requiredMessage4: '文本长度为必填参数',
},
},
AssignmentMethod: {
label: '赋值方式',
custom: {
label: '自定义',
},
ref_variables: {
label: '引用变量 ',
},
},
}

View File

@ -9,46 +9,46 @@ export default {
DatePicker: '日期選擇器',
JsonInput: 'JSON文字框',
RadioCard: '選項卡',
RadioRow: '單行選項卡'
RadioRow: '單行選項卡',
},
default: {
label: '預設值',
placeholder: '請輸入預設值',
requiredMessage: '為必填屬性',
show: '顯示預設值'
show: '顯示預設值',
},
tip: {
requiredMessage: '不能為空',
jsonMessage: 'JSON格式不正確'
jsonMessage: 'JSON格式不正確',
},
searchBar: {
placeholder: '請輸入關鍵字搜索'
placeholder: '請輸入關鍵字搜索',
},
paramForm: {
field: {
label: '參數',
placeholder: '請輸入參數',
requiredMessage: '參數 為必填屬性',
requiredMessage2: '只能輸入字母、數字和底線'
requiredMessage2: '只能輸入字母、數字和底線',
},
name: {
label: '顯示名稱',
placeholder: '請輸入顯示名稱',
requiredMessage: '顯示名稱 為必填屬性'
requiredMessage: '顯示名稱 為必填屬性',
},
tooltip: {
label: '參數提示說明',
placeholder: '請輸入參數提示說明'
placeholder: '請輸入參數提示說明',
},
required: {
label: '是否必填',
requiredMessage: '是否必填 為必填屬性'
requiredMessage: '是否必填 為必填屬性',
},
input_type: {
label: '組件類型',
placeholder: '請選擇組件類型',
requiredMessage: '組件類型 為必填屬性'
}
requiredMessage: '組件類型 為必填屬性',
},
},
DatePicker: {
placeholder: '選擇日期',
@ -58,35 +58,35 @@ export default {
datetime: '日期時間',
dataType: {
label: '時間類型',
placeholder: '請選擇時間類型'
placeholder: '請選擇時間類型',
},
format: {
label: '格式',
placeholder: '請選擇格式'
}
placeholder: '請選擇格式',
},
},
Select: {
label: '選項值',
placeholder: '請輸入選項值'
placeholder: '請輸入選項值',
},
tag: {
label: '標籤',
placeholder: '請輸入選項標籤'
placeholder: '請輸入選項標籤',
},
Slider: {
showInput: {
label: '是否帶輸入框'
label: '是否帶輸入框',
},
valueRange: {
label: '取值範圍',
minRequired: '最小值必填',
maxRequired: '最大值必填'
maxRequired: '最大值必填',
},
step: {
label: '步長值',
requiredMessage1: '步長值必填',
requiredMessage2: '步長不能為0'
}
requiredMessage2: '步長不能為0',
},
},
TextInput: {
length: {
@ -96,7 +96,16 @@ export default {
requiredMessage1: '長度在',
requiredMessage2: '到',
requiredMessage3: '個字元',
requiredMessage4: '文字長度為必填參數'
}
}
requiredMessage4: '文字長度為必填參數',
},
},
AssignmentMethod: {
label: '賦值方式',
custom: {
label: '自定義',
},
ref_variables: {
label: '引用變量',
},
},
}

View File

@ -147,13 +147,14 @@ import NodeContainer from '@/workflow/common/NodeContainer.vue'
import AddFormCollect from '@/workflow/common/AddFormCollect.vue'
import EditFormCollect from '@/workflow/common/EditFormCollect.vue'
import { type FormInstance } from 'element-plus'
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, computed, provide } from 'vue'
import { input_type_list } from '@/components/dynamics-form/constructor/data'
import { MsgError } from '@/utils/message'
import { set, cloneDeep } from 'lodash'
import Sortable from 'sortablejs'
import { t } from '@/locales'
const props = defineProps<{ nodeModel: any }>()
provide('getModel', () => props.nodeModel)
const formNodeFormRef = ref<FormInstance>()
const tableRef = ref()
const editFormField = (form_field_data: any, field_index: number) => {