@flowgram.ai/form-materials 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,123 @@
1
+ import { IRules, Op, OpConfigs } from './types';
2
+
3
+ export const rules: IRules = {
4
+ string: {
5
+ [Op.EQ]: 'string',
6
+ [Op.NEQ]: 'string',
7
+ [Op.CONTAINS]: 'string',
8
+ [Op.NOT_CONTAINS]: 'string',
9
+ [Op.IN]: 'array',
10
+ [Op.NIN]: 'array',
11
+ [Op.IS_EMPTY]: 'string',
12
+ [Op.IS_NOT_EMPTY]: 'string',
13
+ },
14
+ number: {
15
+ [Op.EQ]: 'number',
16
+ [Op.NEQ]: 'number',
17
+ [Op.GT]: 'number',
18
+ [Op.GTE]: 'number',
19
+ [Op.LT]: 'number',
20
+ [Op.LTE]: 'number',
21
+ [Op.IN]: 'array',
22
+ [Op.NIN]: 'array',
23
+ [Op.IS_EMPTY]: null,
24
+ [Op.IS_NOT_EMPTY]: null,
25
+ },
26
+ integer: {
27
+ [Op.EQ]: 'number',
28
+ [Op.NEQ]: 'number',
29
+ [Op.GT]: 'number',
30
+ [Op.GTE]: 'number',
31
+ [Op.LT]: 'number',
32
+ [Op.LTE]: 'number',
33
+ [Op.IN]: 'array',
34
+ [Op.NIN]: 'array',
35
+ [Op.IS_EMPTY]: null,
36
+ [Op.IS_NOT_EMPTY]: null,
37
+ },
38
+ boolean: {
39
+ [Op.EQ]: 'boolean',
40
+ [Op.NEQ]: 'boolean',
41
+ [Op.IS_TRUE]: null,
42
+ [Op.IS_FALSE]: null,
43
+ [Op.IN]: 'array',
44
+ [Op.NIN]: 'array',
45
+ [Op.IS_EMPTY]: null,
46
+ [Op.IS_NOT_EMPTY]: null,
47
+ },
48
+ object: {
49
+ [Op.IS_EMPTY]: null,
50
+ [Op.IS_NOT_EMPTY]: null,
51
+ },
52
+ array: {
53
+ [Op.IS_EMPTY]: null,
54
+ [Op.IS_NOT_EMPTY]: null,
55
+ },
56
+ map: {
57
+ [Op.IS_EMPTY]: null,
58
+ [Op.IS_NOT_EMPTY]: null,
59
+ },
60
+ };
61
+
62
+ export const opConfigs: OpConfigs = {
63
+ [Op.EQ]: {
64
+ label: 'Equal',
65
+ abbreviation: '=',
66
+ },
67
+ [Op.NEQ]: {
68
+ label: 'Not Equal',
69
+ abbreviation: '≠',
70
+ },
71
+ [Op.GT]: {
72
+ label: 'Greater Than',
73
+ abbreviation: '>',
74
+ },
75
+ [Op.GTE]: {
76
+ label: 'Greater Than or Equal',
77
+ abbreviation: '>=',
78
+ },
79
+ [Op.LT]: {
80
+ label: 'Less Than',
81
+ abbreviation: '<',
82
+ },
83
+ [Op.LTE]: {
84
+ label: 'Less Than or Equal',
85
+ abbreviation: '<=',
86
+ },
87
+ [Op.IN]: {
88
+ label: 'In',
89
+ abbreviation: '∈',
90
+ },
91
+ [Op.NIN]: {
92
+ label: 'Not In',
93
+ abbreviation: '∉',
94
+ },
95
+ [Op.CONTAINS]: {
96
+ label: 'Contains',
97
+ abbreviation: '⊇',
98
+ },
99
+ [Op.NOT_CONTAINS]: {
100
+ label: 'Not Contains',
101
+ abbreviation: '⊉',
102
+ },
103
+ [Op.IS_EMPTY]: {
104
+ label: 'Is Empty',
105
+ abbreviation: '=',
106
+ rightDisplay: 'Empty',
107
+ },
108
+ [Op.IS_NOT_EMPTY]: {
109
+ label: 'Is Not Empty',
110
+ abbreviation: '≠',
111
+ rightDisplay: 'Empty',
112
+ },
113
+ [Op.IS_TRUE]: {
114
+ label: 'Is True',
115
+ abbreviation: '=',
116
+ rightDisplay: 'True',
117
+ },
118
+ [Op.IS_FALSE]: {
119
+ label: 'Is False',
120
+ abbreviation: '=',
121
+ rightDisplay: 'False',
122
+ },
123
+ };
@@ -0,0 +1,45 @@
1
+ import React, { useMemo } from 'react';
2
+
3
+ import { Button, Select } from '@douyinfe/semi-ui';
4
+ import { IconChevronDownStroked } from '@douyinfe/semi-icons';
5
+
6
+ import { IRule, Op } from '../types';
7
+ import { opConfigs } from '../constants';
8
+
9
+ interface HookParams {
10
+ rule?: IRule;
11
+ op?: Op;
12
+ onChange: (op: Op) => void;
13
+ }
14
+
15
+ export function useOp({ rule, op, onChange }: HookParams) {
16
+ const options = useMemo(
17
+ () =>
18
+ Object.keys(rule || {}).map((_op) => ({
19
+ ...(opConfigs[_op as Op] || {}),
20
+ value: _op,
21
+ })),
22
+ [rule]
23
+ );
24
+
25
+ const opConfig = useMemo(() => opConfigs[op as Op], [op]);
26
+
27
+ const renderOpSelect = () => (
28
+ <Select
29
+ style={{ height: 22 }}
30
+ size="small"
31
+ value={op}
32
+ optionList={options}
33
+ onChange={(v) => {
34
+ onChange(v as Op);
35
+ }}
36
+ triggerRender={({ value }) => (
37
+ <Button size="small" disabled={!rule}>
38
+ {opConfig?.abbreviation || <IconChevronDownStroked size="small" />}
39
+ </Button>
40
+ )}
41
+ />
42
+ );
43
+
44
+ return { renderOpSelect, opConfig };
45
+ }
@@ -0,0 +1,26 @@
1
+ import { useMemo } from 'react';
2
+
3
+ import { useScopeAvailable } from '@flowgram.ai/editor';
4
+
5
+ import { rules } from '../constants';
6
+ import { JsonSchemaUtils } from '../../../utils';
7
+ import { IFlowRefValue, JsonSchemaBasicType } from '../../../typings';
8
+
9
+ export function useRule(left?: IFlowRefValue) {
10
+ const available = useScopeAvailable();
11
+
12
+ const variable = useMemo(() => {
13
+ if (!left) return undefined;
14
+ return available.getByKeyPath(left.content);
15
+ }, [available, left]);
16
+
17
+ const rule = useMemo(() => {
18
+ if (!variable) return undefined;
19
+
20
+ const schema = JsonSchemaUtils.astToSchema(variable.type, { drilldown: false });
21
+
22
+ return rules[schema?.type as JsonSchemaBasicType];
23
+ }, [variable?.type]);
24
+
25
+ return { rule };
26
+ }
@@ -0,0 +1,71 @@
1
+ import React, { useMemo } from 'react';
2
+
3
+ import { Input } from '@douyinfe/semi-ui';
4
+
5
+ import { ConditionRowValueType, Op } from './types';
6
+ import { UIContainer, UILeft, UIOperator, UIRight, UIValues } from './styles';
7
+ import { useRule } from './hooks/useRule';
8
+ import { useOp } from './hooks/useOp';
9
+ import { VariableSelector } from '../variable-selector';
10
+ import { DynamicValueInput } from '../dynamic-value-input';
11
+ import { JsonSchemaBasicType } from '../../typings';
12
+
13
+ interface PropTypes {
14
+ value?: ConditionRowValueType;
15
+ onChange: (value?: ConditionRowValueType) => void;
16
+ style?: React.CSSProperties;
17
+ readonly?: boolean;
18
+ }
19
+
20
+ export function ConditionRow({ style, value, onChange, readonly }: PropTypes) {
21
+ const { left, operator, right } = value || {};
22
+ const { rule } = useRule(left);
23
+ const { renderOpSelect, opConfig } = useOp({
24
+ rule,
25
+ op: operator,
26
+ onChange: (v) => onChange({ ...value, operator: v }),
27
+ });
28
+
29
+ const targetSchema = useMemo(() => {
30
+ const targetType: JsonSchemaBasicType | null = rule?.[operator as Op] || null;
31
+ return targetType ? { type: targetType, extra: { weak: true } } : null;
32
+ }, [rule, opConfig]);
33
+
34
+ return (
35
+ <UIContainer style={style}>
36
+ <UIOperator>{renderOpSelect()}</UIOperator>
37
+ <UIValues>
38
+ <UILeft>
39
+ <VariableSelector
40
+ readonly={readonly}
41
+ style={{ width: '100%' }}
42
+ value={left?.content}
43
+ onChange={(v) =>
44
+ onChange({
45
+ ...value,
46
+ left: {
47
+ type: 'ref',
48
+ content: v,
49
+ },
50
+ })
51
+ }
52
+ />
53
+ </UILeft>
54
+ <UIRight>
55
+ {targetSchema ? (
56
+ <DynamicValueInput
57
+ readonly={readonly || !rule}
58
+ value={right}
59
+ schema={targetSchema}
60
+ onChange={(v) => onChange({ ...value, right: v })}
61
+ />
62
+ ) : (
63
+ <Input size="small" disabled value={opConfig?.rightDisplay || 'Empty'} />
64
+ )}
65
+ </UIRight>
66
+ </UIValues>
67
+ </UIContainer>
68
+ );
69
+ }
70
+
71
+ export { ConditionRowValueType };
@@ -0,0 +1,25 @@
1
+ import styled from 'styled-components';
2
+
3
+ export const UIContainer = styled.div`
4
+ display: flex;
5
+ align-items: center;
6
+ gap: 4px;
7
+ `;
8
+
9
+ export const UIOperator = styled.div``;
10
+
11
+ export const UILeft = styled.div`
12
+ width: 100%;
13
+ `;
14
+
15
+ export const UIRight = styled.div`
16
+ width: 100%;
17
+ `;
18
+
19
+ export const UIValues = styled.div`
20
+ flex-grow: 1;
21
+ display: flex;
22
+ flex-direction: column;
23
+ align-items: center;
24
+ gap: 4px;
25
+ `;
@@ -0,0 +1,37 @@
1
+ import { IFlowConstantRefValue, IFlowRefValue, JsonSchemaBasicType } from '../../typings';
2
+
3
+ export enum Op {
4
+ EQ = 'eq',
5
+ NEQ = 'neq',
6
+ GT = 'gt',
7
+ GTE = 'gte',
8
+ LT = 'lt',
9
+ LTE = 'lte',
10
+ IN = 'in',
11
+ NIN = 'nin',
12
+ CONTAINS = 'contains',
13
+ NOT_CONTAINS = 'not_contains',
14
+ IS_EMPTY = 'is_empty',
15
+ IS_NOT_EMPTY = 'is_not_empty',
16
+ IS_TRUE = 'is_true',
17
+ IS_FALSE = 'is_false',
18
+ }
19
+
20
+ export interface OpConfig {
21
+ label: string;
22
+ abbreviation: string;
23
+ // When right is not a value, display this text
24
+ rightDisplay?: string;
25
+ }
26
+
27
+ export type OpConfigs = Record<Op, OpConfig>;
28
+
29
+ export type IRule = Partial<Record<Op, JsonSchemaBasicType | null>>;
30
+
31
+ export type IRules = Record<JsonSchemaBasicType, IRule>;
32
+
33
+ export interface ConditionRowValueType {
34
+ left?: IFlowRefValue;
35
+ operator?: Op;
36
+ right?: IFlowConstantRefValue;
37
+ }
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { useMemo } from 'react';
2
2
 
3
3
  import { IconButton } from '@douyinfe/semi-ui';
4
4
  import { IconSetting } from '@douyinfe/semi-icons';
@@ -31,6 +31,14 @@ export function DynamicValueInput({
31
31
  schema,
32
32
  constantProps,
33
33
  }: PropsType) {
34
+ // When is number type, include integer as well
35
+ const includeSchema = useMemo(() => {
36
+ if (schema?.type === 'number') {
37
+ return [schema, { type: 'integer' }];
38
+ }
39
+ return schema;
40
+ }, [schema]);
41
+
34
42
  const renderMain = () => {
35
43
  if (value?.type === 'ref') {
36
44
  // Display Variable Or Delete
@@ -38,7 +46,7 @@ export function DynamicValueInput({
38
46
  <VariableSelector
39
47
  value={value?.content}
40
48
  onChange={(_v) => onChange(_v ? { type: 'ref', content: _v } : undefined)}
41
- includeSchema={schema}
49
+ includeSchema={includeSchema}
42
50
  readonly={readonly}
43
51
  />
44
52
  );
@@ -60,7 +68,7 @@ export function DynamicValueInput({
60
68
  style={{ width: '100%' }}
61
69
  value={value?.type === 'ref' ? value?.content : undefined}
62
70
  onChange={(_v) => onChange({ type: 'ref', content: _v })}
63
- includeSchema={schema}
71
+ includeSchema={includeSchema}
64
72
  readonly={readonly}
65
73
  triggerRender={() => (
66
74
  <IconButton disabled={readonly} size="small" icon={<IconSetting size="small" />} />
@@ -4,3 +4,4 @@ export * from './json-schema-editor';
4
4
  export * from './batch-variable-selector';
5
5
  export * from './constant-input';
6
6
  export * from './dynamic-value-input';
7
+ export * from './condition-row';
@@ -0,0 +1,22 @@
1
+ import React, { useEffect, useState } from 'react';
2
+
3
+ import Input, { InputProps } from '@douyinfe/semi-ui/lib/es/input';
4
+
5
+ export function BlurInput(props: InputProps) {
6
+ const [value, setValue] = useState('');
7
+
8
+ useEffect(() => {
9
+ setValue(props.value as string);
10
+ }, [props.value]);
11
+
12
+ return (
13
+ <Input
14
+ {...props}
15
+ value={value}
16
+ onChange={(value) => {
17
+ setValue(value);
18
+ }}
19
+ onBlur={(e) => props.onChange?.(value, e)}
20
+ />
21
+ );
22
+ }
@@ -1,6 +1,6 @@
1
1
  import React, { useMemo, useState } from 'react';
2
2
 
3
- import { Button, Checkbox, IconButton, Input } from '@douyinfe/semi-ui';
3
+ import { Button, Checkbox, IconButton } from '@douyinfe/semi-ui';
4
4
  import {
5
5
  IconExpand,
6
6
  IconShrink,
@@ -31,6 +31,7 @@ import {
31
31
  import { UIName } from './styles';
32
32
  import { UIRow } from './styles';
33
33
  import { usePropertiesEdit } from './hooks';
34
+ import { BlurInput } from './components/blur-input';
34
35
 
35
36
  export function JsonSchemaEditor(props: {
36
37
  value?: IJsonSchema;
@@ -109,7 +110,7 @@ function PropertyEdit(props: {
109
110
  <UIPropertyMain $expand={expand}>
110
111
  <UIRow>
111
112
  <UIName>
112
- <Input
113
+ <BlurInput
113
114
  placeholder={config?.placeholder ?? 'Input Variable Name'}
114
115
  size="small"
115
116
  value={name}
@@ -162,7 +163,7 @@ function PropertyEdit(props: {
162
163
  {expand && (
163
164
  <UIExpandDetail>
164
165
  <UILabel>{config?.descTitle ?? 'Description'}</UILabel>
165
- <Input
166
+ <BlurInput
166
167
  size="small"
167
168
  value={description}
168
169
  onChange={(value) => onChange('description', value)}
@@ -100,7 +100,7 @@ export const VariableSelector = ({
100
100
  );
101
101
  }}
102
102
  showClear={false}
103
- arrowIcon={value ? null : <IconChevronDownStroked size="small" />}
103
+ arrowIcon={<IconChevronDownStroked size="small" />}
104
104
  triggerRender={triggerRender}
105
105
  placeholder={config?.placeholder ?? 'Select Variable...'}
106
106
  />
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "auto-rename-ref",
3
+ "depMaterials": ["flow-value"],
4
+ "depPackages": ["lodash"]
5
+ }
@@ -0,0 +1,104 @@
1
+ import { isArray, isObject } from 'lodash';
2
+ import {
3
+ DataEvent,
4
+ Effect,
5
+ EffectOptions,
6
+ VariableFieldKeyRenameService,
7
+ } from '@flowgram.ai/editor';
8
+
9
+ import { IFlowRefValue } from '../../typings';
10
+
11
+ /**
12
+ * Auto rename ref when form item's key is renamed
13
+ *
14
+ * Example:
15
+ *
16
+ * formMeta: {
17
+ * effects: {
18
+ * "inputsValues": autoRenameRefEffect,
19
+ * }
20
+ * }
21
+ */
22
+ export const autoRenameRefEffect: EffectOptions[] = [
23
+ {
24
+ event: DataEvent.onValueInit,
25
+ effect: ((params) => {
26
+ const { context, form, name } = params;
27
+
28
+ const renameService = context.node.getService(VariableFieldKeyRenameService);
29
+
30
+ const disposable = renameService.onRename(({ before, after }) => {
31
+ const beforeKeyPath = [
32
+ ...before.parentFields.map((_field) => _field.key).reverse(),
33
+ before.key,
34
+ ];
35
+ const afterKeyPath = [
36
+ ...after.parentFields.map((_field) => _field.key).reverse(),
37
+ after.key,
38
+ ];
39
+
40
+ // traverse rename refs inside form item 'name'
41
+ traverseRef(name, form.getValueIn(name), (_drilldownName, _v) => {
42
+ if (isRefMatch(_v, beforeKeyPath)) {
43
+ _v.content = [...afterKeyPath, ...(_v.content || [])?.slice(beforeKeyPath.length)];
44
+ form.setValueIn(_drilldownName, _v);
45
+ }
46
+ });
47
+ });
48
+
49
+ return () => {
50
+ disposable.dispose();
51
+ };
52
+ }) as Effect,
53
+ },
54
+ ];
55
+
56
+ /**
57
+ * If ref value's keyPath is the under as targetKeyPath
58
+ * @param value
59
+ * @param targetKeyPath
60
+ * @returns
61
+ */
62
+ function isRefMatch(value: IFlowRefValue, targetKeyPath: string[]) {
63
+ return targetKeyPath.every((_key, index) => _key === value.content?.[index]);
64
+ }
65
+
66
+ /**
67
+ * If value is ref
68
+ * @param value
69
+ * @returns
70
+ */
71
+ function isRef(value: any): value is IFlowRefValue {
72
+ return (
73
+ value?.type === 'ref' && Array.isArray(value?.content) && typeof value?.content[0] === 'string'
74
+ );
75
+ }
76
+
77
+ /**
78
+ * Traverse value to find ref
79
+ * @param value
80
+ * @param options
81
+ * @returns
82
+ */
83
+ function traverseRef(name: string, value: any, cb: (name: string, _v: IFlowRefValue) => void) {
84
+ if (isObject(value)) {
85
+ if (isRef(value)) {
86
+ cb(name, value);
87
+ return;
88
+ }
89
+
90
+ Object.entries(value).forEach(([_key, _value]) => {
91
+ traverseRef(`${name}.${_key}`, _value, cb);
92
+ });
93
+ return;
94
+ }
95
+
96
+ if (isArray(value)) {
97
+ value.forEach((_value, idx) => {
98
+ traverseRef(`${name}[${idx}]`, _value, cb);
99
+ });
100
+ return;
101
+ }
102
+
103
+ return;
104
+ }
@@ -1,2 +1,3 @@
1
1
  export * from './provide-batch-input';
2
2
  export * from './provide-batch-outputs';
3
+ export * from './auto-rename-ref';
@@ -74,7 +74,12 @@ export namespace JsonSchemaUtils {
74
74
  * @param typeAST
75
75
  * @returns
76
76
  */
77
- export function astToSchema(typeAST: ASTNode): IJsonSchema | undefined {
77
+ export function astToSchema(
78
+ typeAST: ASTNode,
79
+ options?: { drilldown?: boolean }
80
+ ): IJsonSchema | undefined {
81
+ const { drilldown = true } = options || {};
82
+
78
83
  if (ASTMatch.isString(typeAST)) {
79
84
  return {
80
85
  type: 'string',
@@ -102,23 +107,25 @@ export namespace JsonSchemaUtils {
102
107
  if (ASTMatch.isObject(typeAST)) {
103
108
  return {
104
109
  type: 'object',
105
- properties: Object.fromEntries(
106
- Object.entries(typeAST.properties).map(([key, value]) => [key, astToSchema(value)!])
107
- ),
110
+ properties: drilldown
111
+ ? Object.fromEntries(
112
+ Object.entries(typeAST.properties).map(([key, value]) => [key, astToSchema(value)!])
113
+ )
114
+ : {},
108
115
  };
109
116
  }
110
117
 
111
118
  if (ASTMatch.isArray(typeAST)) {
112
119
  return {
113
120
  type: 'array',
114
- items: astToSchema(typeAST.items),
121
+ items: drilldown ? astToSchema(typeAST.items) : undefined,
115
122
  };
116
123
  }
117
124
 
118
125
  if (ASTMatch.isMap(typeAST)) {
119
126
  return {
120
127
  type: 'map',
121
- items: astToSchema(typeAST.valueType),
128
+ items: drilldown ? astToSchema(typeAST.valueType) : undefined,
122
129
  };
123
130
  }
124
131