mirror of
https://gitee.com/ByteDance/flowgram.ai.git
synced 2025-07-07 17:43:29 +08:00
feat(material): condition row (#268)
* feat(material): condition row * docs(material): condition row docs
This commit is contained in:
parent
9c69aa4e52
commit
bac29feb3a
@ -57,15 +57,25 @@ export const initialData: FlowDocumentJSON = {
|
||||
{
|
||||
key: 'if_0',
|
||||
value: {
|
||||
type: 'expression',
|
||||
content: '',
|
||||
left: {
|
||||
type: 'ref',
|
||||
content: ['start_0', 'query'],
|
||||
},
|
||||
operator: 'contains',
|
||||
right: {
|
||||
type: 'constant',
|
||||
content: 'Hello Flow.',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'if_f0rOAt',
|
||||
value: {
|
||||
type: 'expression',
|
||||
content: '',
|
||||
left: {
|
||||
type: 'ref',
|
||||
content: ['start_0', 'enable'],
|
||||
},
|
||||
operator: 'is_true',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
import { Field, FieldArray } from '@flowgram.ai/free-layout-editor';
|
||||
import { IFlowValue, VariableSelector } from '@flowgram.ai/form-materials';
|
||||
import { ConditionRow, ConditionRowValueType, VariableSelector } from '@flowgram.ai/form-materials';
|
||||
import { Button } from '@douyinfe/semi-ui';
|
||||
import { IconPlus, IconCrossCircleStroked } from '@douyinfe/semi-icons';
|
||||
|
||||
@ -11,7 +11,7 @@ import { ConditionPort } from './styles';
|
||||
|
||||
interface ConditionValue {
|
||||
key: string;
|
||||
value: IFlowValue;
|
||||
value?: ConditionRowValueType;
|
||||
}
|
||||
|
||||
export function ConditionInputs() {
|
||||
@ -25,17 +25,11 @@ export function ConditionInputs() {
|
||||
{({ field: childField, fieldState: childState }) => (
|
||||
<FormItem name="if" type="boolean" required={true} labelWidth={40}>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<VariableSelector
|
||||
style={{ width: '100%' }}
|
||||
value={childField.value?.value?.content as string[]}
|
||||
onChange={(v) =>
|
||||
childField.onChange({
|
||||
key: childField.value.key,
|
||||
value: { type: 'ref', content: v },
|
||||
})
|
||||
}
|
||||
hasError={Object.keys(childState?.errors || {}).length > 0}
|
||||
<ConditionRow
|
||||
readonly={readonly}
|
||||
style={{ flexGrow: 1 }}
|
||||
value={childField.value.value}
|
||||
onChange={(v) => childField.onChange({ value: v, key: childField.value.key })}
|
||||
/>
|
||||
|
||||
<Button
|
||||
|
||||
@ -18,8 +18,8 @@ export const formMeta: FormMeta<FlowNodeJSON> = {
|
||||
validateTrigger: ValidateTrigger.onChange,
|
||||
validate: {
|
||||
title: ({ value }: { value: string }) => (value ? undefined : 'Title is required'),
|
||||
'inputsValues.conditions.*': ({ value }) => {
|
||||
if (!value?.value?.content) return 'Condition is required';
|
||||
'conditions.*': ({ value }) => {
|
||||
if (!value?.value) return 'Condition is required';
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
|
||||
@ -25,38 +25,17 @@ export const ConditionNodeRegistry: FlowNodeRegistry = {
|
||||
type: 'condition',
|
||||
data: {
|
||||
title: 'Condition',
|
||||
inputsValues: {
|
||||
conditions: [
|
||||
{
|
||||
key: `if_${nanoid(5)}`,
|
||||
value: '',
|
||||
value: {},
|
||||
},
|
||||
{
|
||||
key: `if_${nanoid(5)}`,
|
||||
value: '',
|
||||
value: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
inputs: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
conditions: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
key: {
|
||||
type: 'string',
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@ -96,6 +96,16 @@ After the CLI runs successfully, the relevant materials will be automatically ad
|
||||
DynamicValueInput is used for configuring values (constant values + variable values).
|
||||
</MaterialDisplay>
|
||||
|
||||
### ConditionRow
|
||||
|
||||
<MaterialDisplay
|
||||
imgs={[{ src: '/materials/condition-row.png', caption: 'The first condition checks if the query variable contains Hello Flow, the second condition checks if the enable variable is true.' }]}
|
||||
filePath="components/condition-row/index.tsx"
|
||||
exportName="ConditionRow"
|
||||
>
|
||||
ConditionRow is used for configuring a **single row** of condition judgment.
|
||||
</MaterialDisplay>
|
||||
|
||||
## Currently Supported Effect Materials
|
||||
|
||||
### provideBatchInput
|
||||
|
||||
BIN
apps/docs/src/public/materials/condition-row.png
Normal file
BIN
apps/docs/src/public/materials/condition-row.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
@ -97,6 +97,19 @@ CLI 运行成功后,相关物料会自动添加到当前项目下的 `src/form
|
||||
DynamicValueInput 用于值(常量值 + 变量值)的配置
|
||||
</MaterialDisplay>
|
||||
|
||||
|
||||
### ConditionRow
|
||||
|
||||
<MaterialDisplay
|
||||
imgs={[{ src: '/materials/condition-row.png', caption: '第一个条件为 query 变量包含 Hello Flow,第二个条件为 enable 变量为 true' }]}
|
||||
filePath="components/condition-row/index.tsx"
|
||||
exportName="ConditionRow"
|
||||
>
|
||||
ConditionRow 用于 **一行** 条件判断的配置
|
||||
</MaterialDisplay>
|
||||
|
||||
|
||||
|
||||
## 当前支持的 Effect 物料
|
||||
|
||||
### provideBatchInput
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "condition-row",
|
||||
"depMaterials": ["variable-selector", "dynamic-value-input", "flow-value", "utils/json-schema", "typings/json-schema"],
|
||||
"depPackages": ["@douyinfe/semi-ui", "styled-components"]
|
||||
}
|
||||
@ -0,0 +1,123 @@
|
||||
import { IRules, Op, OpConfigs } from './types';
|
||||
|
||||
export const rules: IRules = {
|
||||
string: {
|
||||
[Op.EQ]: 'string',
|
||||
[Op.NEQ]: 'string',
|
||||
[Op.CONTAINS]: 'string',
|
||||
[Op.NOT_CONTAINS]: 'string',
|
||||
[Op.IN]: 'array',
|
||||
[Op.NIN]: 'array',
|
||||
[Op.IS_EMPTY]: 'string',
|
||||
[Op.IS_NOT_EMPTY]: 'string',
|
||||
},
|
||||
number: {
|
||||
[Op.EQ]: 'number',
|
||||
[Op.NEQ]: 'number',
|
||||
[Op.GT]: 'number',
|
||||
[Op.GTE]: 'number',
|
||||
[Op.LT]: 'number',
|
||||
[Op.LTE]: 'number',
|
||||
[Op.IN]: 'array',
|
||||
[Op.NIN]: 'array',
|
||||
[Op.IS_EMPTY]: null,
|
||||
[Op.IS_NOT_EMPTY]: null,
|
||||
},
|
||||
integer: {
|
||||
[Op.EQ]: 'number',
|
||||
[Op.NEQ]: 'number',
|
||||
[Op.GT]: 'number',
|
||||
[Op.GTE]: 'number',
|
||||
[Op.LT]: 'number',
|
||||
[Op.LTE]: 'number',
|
||||
[Op.IN]: 'array',
|
||||
[Op.NIN]: 'array',
|
||||
[Op.IS_EMPTY]: null,
|
||||
[Op.IS_NOT_EMPTY]: null,
|
||||
},
|
||||
boolean: {
|
||||
[Op.EQ]: 'boolean',
|
||||
[Op.NEQ]: 'boolean',
|
||||
[Op.IS_TRUE]: null,
|
||||
[Op.IS_FALSE]: null,
|
||||
[Op.IN]: 'array',
|
||||
[Op.NIN]: 'array',
|
||||
[Op.IS_EMPTY]: null,
|
||||
[Op.IS_NOT_EMPTY]: null,
|
||||
},
|
||||
object: {
|
||||
[Op.IS_EMPTY]: null,
|
||||
[Op.IS_NOT_EMPTY]: null,
|
||||
},
|
||||
array: {
|
||||
[Op.IS_EMPTY]: null,
|
||||
[Op.IS_NOT_EMPTY]: null,
|
||||
},
|
||||
map: {
|
||||
[Op.IS_EMPTY]: null,
|
||||
[Op.IS_NOT_EMPTY]: null,
|
||||
},
|
||||
};
|
||||
|
||||
export const opConfigs: OpConfigs = {
|
||||
[Op.EQ]: {
|
||||
label: 'Equal',
|
||||
abbreviation: '=',
|
||||
},
|
||||
[Op.NEQ]: {
|
||||
label: 'Not Equal',
|
||||
abbreviation: '≠',
|
||||
},
|
||||
[Op.GT]: {
|
||||
label: 'Greater Than',
|
||||
abbreviation: '>',
|
||||
},
|
||||
[Op.GTE]: {
|
||||
label: 'Greater Than or Equal',
|
||||
abbreviation: '>=',
|
||||
},
|
||||
[Op.LT]: {
|
||||
label: 'Less Than',
|
||||
abbreviation: '<',
|
||||
},
|
||||
[Op.LTE]: {
|
||||
label: 'Less Than or Equal',
|
||||
abbreviation: '<=',
|
||||
},
|
||||
[Op.IN]: {
|
||||
label: 'In',
|
||||
abbreviation: '∈',
|
||||
},
|
||||
[Op.NIN]: {
|
||||
label: 'Not In',
|
||||
abbreviation: '∉',
|
||||
},
|
||||
[Op.CONTAINS]: {
|
||||
label: 'Contains',
|
||||
abbreviation: '⊇',
|
||||
},
|
||||
[Op.NOT_CONTAINS]: {
|
||||
label: 'Not Contains',
|
||||
abbreviation: '⊉',
|
||||
},
|
||||
[Op.IS_EMPTY]: {
|
||||
label: 'Is Empty',
|
||||
abbreviation: '=',
|
||||
rightDisplay: 'Empty',
|
||||
},
|
||||
[Op.IS_NOT_EMPTY]: {
|
||||
label: 'Is Not Empty',
|
||||
abbreviation: '≠',
|
||||
rightDisplay: 'Empty',
|
||||
},
|
||||
[Op.IS_TRUE]: {
|
||||
label: 'Is True',
|
||||
abbreviation: '=',
|
||||
rightDisplay: 'True',
|
||||
},
|
||||
[Op.IS_FALSE]: {
|
||||
label: 'Is False',
|
||||
abbreviation: '=',
|
||||
rightDisplay: 'False',
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,45 @@
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { Button, Select } from '@douyinfe/semi-ui';
|
||||
import { IconChevronDownStroked } from '@douyinfe/semi-icons';
|
||||
|
||||
import { IRule, Op } from '../types';
|
||||
import { opConfigs } from '../constants';
|
||||
|
||||
interface HookParams {
|
||||
rule?: IRule;
|
||||
op?: Op;
|
||||
onChange: (op: Op) => void;
|
||||
}
|
||||
|
||||
export function useOp({ rule, op, onChange }: HookParams) {
|
||||
const options = useMemo(
|
||||
() =>
|
||||
Object.keys(rule || {}).map((_op) => ({
|
||||
...(opConfigs[_op as Op] || {}),
|
||||
value: _op,
|
||||
})),
|
||||
[rule]
|
||||
);
|
||||
|
||||
const opConfig = useMemo(() => opConfigs[op as Op], [op]);
|
||||
|
||||
const renderOpSelect = () => (
|
||||
<Select
|
||||
style={{ height: 22 }}
|
||||
size="small"
|
||||
value={op}
|
||||
optionList={options}
|
||||
onChange={(v) => {
|
||||
onChange(v as Op);
|
||||
}}
|
||||
triggerRender={({ value }) => (
|
||||
<Button size="small" disabled={!rule}>
|
||||
{opConfig?.abbreviation || <IconChevronDownStroked size="small" />}
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
return { renderOpSelect, opConfig };
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useScopeAvailable } from '@flowgram.ai/editor';
|
||||
|
||||
import { rules } from '../constants';
|
||||
import { JsonSchemaUtils } from '../../../utils';
|
||||
import { IFlowRefValue, JsonSchemaBasicType } from '../../../typings';
|
||||
|
||||
export function useRule(left?: IFlowRefValue) {
|
||||
const available = useScopeAvailable();
|
||||
|
||||
const variable = useMemo(() => {
|
||||
if (!left) return undefined;
|
||||
return available.getByKeyPath(left.content);
|
||||
}, [available, left]);
|
||||
|
||||
const rule = useMemo(() => {
|
||||
if (!variable) return undefined;
|
||||
|
||||
const schema = JsonSchemaUtils.astToSchema(variable.type, { drilldown: false });
|
||||
|
||||
return rules[schema?.type as JsonSchemaBasicType];
|
||||
}, [variable?.type]);
|
||||
|
||||
return { rule };
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { Input } from '@douyinfe/semi-ui';
|
||||
|
||||
import { ConditionRowValueType, Op } from './types';
|
||||
import { UIContainer, UILeft, UIOperator, UIRight, UIValues } from './styles';
|
||||
import { useRule } from './hooks/useRule';
|
||||
import { useOp } from './hooks/useOp';
|
||||
import { VariableSelector } from '../variable-selector';
|
||||
import { DynamicValueInput } from '../dynamic-value-input';
|
||||
import { JsonSchemaBasicType } from '../../typings';
|
||||
|
||||
interface PropTypes {
|
||||
value?: ConditionRowValueType;
|
||||
onChange: (value?: ConditionRowValueType) => void;
|
||||
style?: React.CSSProperties;
|
||||
readonly?: boolean;
|
||||
}
|
||||
|
||||
export function ConditionRow({ style, value, onChange, readonly }: PropTypes) {
|
||||
const { left, operator, right } = value || {};
|
||||
const { rule } = useRule(left);
|
||||
const { renderOpSelect, opConfig } = useOp({
|
||||
rule,
|
||||
op: operator,
|
||||
onChange: (v) => onChange({ ...value, operator: v }),
|
||||
});
|
||||
|
||||
const targetSchema = useMemo(() => {
|
||||
const targetType: JsonSchemaBasicType | null = rule?.[operator as Op] || null;
|
||||
return targetType ? { type: targetType, extra: { weak: true } } : null;
|
||||
}, [rule, opConfig]);
|
||||
|
||||
return (
|
||||
<UIContainer style={style}>
|
||||
<UIOperator>{renderOpSelect()}</UIOperator>
|
||||
<UIValues>
|
||||
<UILeft>
|
||||
<VariableSelector
|
||||
readonly={readonly}
|
||||
style={{ width: '100%' }}
|
||||
value={left?.content}
|
||||
onChange={(v) =>
|
||||
onChange({
|
||||
...value,
|
||||
left: {
|
||||
type: 'ref',
|
||||
content: v,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</UILeft>
|
||||
<UIRight>
|
||||
{targetSchema ? (
|
||||
<DynamicValueInput
|
||||
readonly={readonly || !rule}
|
||||
value={right}
|
||||
schema={targetSchema}
|
||||
onChange={(v) => onChange({ ...value, right: v })}
|
||||
/>
|
||||
) : (
|
||||
<Input size="small" disabled value={opConfig?.rightDisplay || 'Empty'} />
|
||||
)}
|
||||
</UIRight>
|
||||
</UIValues>
|
||||
</UIContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export { ConditionRowValueType };
|
||||
@ -0,0 +1,25 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const UIContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
`;
|
||||
|
||||
export const UIOperator = styled.div``;
|
||||
|
||||
export const UILeft = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const UIRight = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const UIValues = styled.div`
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
`;
|
||||
@ -0,0 +1,37 @@
|
||||
import { IFlowConstantRefValue, IFlowRefValue, JsonSchemaBasicType } from '../../typings';
|
||||
|
||||
export enum Op {
|
||||
EQ = 'eq',
|
||||
NEQ = 'neq',
|
||||
GT = 'gt',
|
||||
GTE = 'gte',
|
||||
LT = 'lt',
|
||||
LTE = 'lte',
|
||||
IN = 'in',
|
||||
NIN = 'nin',
|
||||
CONTAINS = 'contains',
|
||||
NOT_CONTAINS = 'not_contains',
|
||||
IS_EMPTY = 'is_empty',
|
||||
IS_NOT_EMPTY = 'is_not_empty',
|
||||
IS_TRUE = 'is_true',
|
||||
IS_FALSE = 'is_false',
|
||||
}
|
||||
|
||||
export interface OpConfig {
|
||||
label: string;
|
||||
abbreviation: string;
|
||||
// When right is not a value, display this text
|
||||
rightDisplay?: string;
|
||||
}
|
||||
|
||||
export type OpConfigs = Record<Op, OpConfig>;
|
||||
|
||||
export type IRule = Partial<Record<Op, JsonSchemaBasicType | null>>;
|
||||
|
||||
export type IRules = Record<JsonSchemaBasicType, IRule>;
|
||||
|
||||
export interface ConditionRowValueType {
|
||||
left?: IFlowRefValue;
|
||||
operator?: Op;
|
||||
right?: IFlowConstantRefValue;
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { IconButton } from '@douyinfe/semi-ui';
|
||||
import { IconSetting } from '@douyinfe/semi-icons';
|
||||
@ -31,6 +31,14 @@ export function DynamicValueInput({
|
||||
schema,
|
||||
constantProps,
|
||||
}: PropsType) {
|
||||
// When is number type, include integer as well
|
||||
const includeSchema = useMemo(() => {
|
||||
if (schema?.type === 'number') {
|
||||
return [schema, { type: 'integer' }];
|
||||
}
|
||||
return schema;
|
||||
}, [schema]);
|
||||
|
||||
const renderMain = () => {
|
||||
if (value?.type === 'ref') {
|
||||
// Display Variable Or Delete
|
||||
@ -38,7 +46,7 @@ export function DynamicValueInput({
|
||||
<VariableSelector
|
||||
value={value?.content}
|
||||
onChange={(_v) => onChange(_v ? { type: 'ref', content: _v } : undefined)}
|
||||
includeSchema={schema}
|
||||
includeSchema={includeSchema}
|
||||
readonly={readonly}
|
||||
/>
|
||||
);
|
||||
@ -60,7 +68,7 @@ export function DynamicValueInput({
|
||||
style={{ width: '100%' }}
|
||||
value={value?.type === 'ref' ? value?.content : undefined}
|
||||
onChange={(_v) => onChange({ type: 'ref', content: _v })}
|
||||
includeSchema={schema}
|
||||
includeSchema={includeSchema}
|
||||
readonly={readonly}
|
||||
triggerRender={() => (
|
||||
<IconButton disabled={readonly} size="small" icon={<IconSetting size="small" />} />
|
||||
|
||||
@ -4,3 +4,4 @@ export * from './json-schema-editor';
|
||||
export * from './batch-variable-selector';
|
||||
export * from './constant-input';
|
||||
export * from './dynamic-value-input';
|
||||
export * from './condition-row';
|
||||
|
||||
@ -74,7 +74,12 @@ export namespace JsonSchemaUtils {
|
||||
* @param typeAST
|
||||
* @returns
|
||||
*/
|
||||
export function astToSchema(typeAST: ASTNode): IJsonSchema | undefined {
|
||||
export function astToSchema(
|
||||
typeAST: ASTNode,
|
||||
options?: { drilldown?: boolean }
|
||||
): IJsonSchema | undefined {
|
||||
const { drilldown = true } = options || {};
|
||||
|
||||
if (ASTMatch.isString(typeAST)) {
|
||||
return {
|
||||
type: 'string',
|
||||
@ -102,23 +107,25 @@ export namespace JsonSchemaUtils {
|
||||
if (ASTMatch.isObject(typeAST)) {
|
||||
return {
|
||||
type: 'object',
|
||||
properties: Object.fromEntries(
|
||||
properties: drilldown
|
||||
? Object.fromEntries(
|
||||
Object.entries(typeAST.properties).map(([key, value]) => [key, astToSchema(value)!])
|
||||
),
|
||||
)
|
||||
: {},
|
||||
};
|
||||
}
|
||||
|
||||
if (ASTMatch.isArray(typeAST)) {
|
||||
return {
|
||||
type: 'array',
|
||||
items: astToSchema(typeAST.items),
|
||||
items: drilldown ? astToSchema(typeAST.items) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
if (ASTMatch.isMap(typeAST)) {
|
||||
return {
|
||||
type: 'map',
|
||||
items: astToSchema(typeAST.valueType),
|
||||
items: drilldown ? astToSchema(typeAST.valueType) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user