Parcourir la source

feat(material): condition row (#268)

* feat(material): condition row

* docs(material): condition row docs
Yiwei Mao il y a 7 mois
Parent
commit
bac29feb3a

+ 14 - 4
apps/demo-free-layout/src/initial-data.ts

@@ -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',
             },
           },
         ],

+ 6 - 12
apps/demo-free-layout/src/nodes/condition/condition-inputs/index.tsx

@@ -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

+ 2 - 2
apps/demo-free-layout/src/nodes/condition/form-meta.tsx

@@ -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;
     },
   },

+ 9 - 30
apps/demo-free-layout/src/nodes/condition/index.ts

@@ -25,37 +25,16 @@ export const ConditionNodeRegistry: FlowNodeRegistry = {
       type: 'condition',
       data: {
         title: 'Condition',
-        inputsValues: {
-          conditions: [
-            {
-              key: `if_${nanoid(5)}`,
-              value: '',
-            },
-            {
-              key: `if_${nanoid(5)}`,
-              value: '',
-            },
-          ],
-        },
-        inputs: {
-          type: 'object',
-          properties: {
-            conditions: {
-              type: 'array',
-              items: {
-                type: 'object',
-                properties: {
-                  key: {
-                    type: 'string',
-                  },
-                  value: {
-                    type: 'string',
-                  },
-                },
-              },
-            },
+        conditions: [
+          {
+            key: `if_${nanoid(5)}`,
+            value: {},
           },
-        },
+          {
+            key: `if_${nanoid(5)}`,
+            value: {},
+          },
+        ],
       },
     };
   },

+ 10 - 0
apps/docs/src/en/guide/advanced/form-materials.mdx

@@ -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


+ 13 - 0
apps/docs/src/zh/guide/advanced/form-materials.mdx

@@ -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

+ 5 - 0
packages/materials/form-materials/src/components/condition-row/config.json

@@ -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"]
+}

+ 123 - 0
packages/materials/form-materials/src/components/condition-row/constants.ts

@@ -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',
+  },
+};

+ 45 - 0
packages/materials/form-materials/src/components/condition-row/hooks/useOp.tsx

@@ -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 };
+}

+ 26 - 0
packages/materials/form-materials/src/components/condition-row/hooks/useRule.ts

@@ -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 };
+}

+ 71 - 0
packages/materials/form-materials/src/components/condition-row/index.tsx

@@ -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 };

+ 25 - 0
packages/materials/form-materials/src/components/condition-row/styles.tsx

@@ -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;
+`;

+ 37 - 0
packages/materials/form-materials/src/components/condition-row/types.ts

@@ -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;
+}

+ 11 - 3
packages/materials/form-materials/src/components/dynamic-value-input/index.tsx

@@ -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" />} />

+ 1 - 0
packages/materials/form-materials/src/components/index.ts

@@ -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';

+ 13 - 6
packages/materials/form-materials/src/utils/json-schema/index.ts

@@ -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(
-          Object.entries(typeAST.properties).map(([key, value]) => [key, astToSchema(value)!])
-        ),
+        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,
       };
     }