feat: form/button linkage rules (#1456)

* feat(plugin-workflow): add prompt node

* feat(plugin-workflow): useValueGetter for all instructions and triggers

* feat(plugin-workflow): add workflow block initializer

* refactor(plugin-workflow): change prompt node type to manual

* feat(plugin-workflow): add ModeConfig component for mode

* feat(plugin-workflow): add todo drawer

* feat(plugin-workflow): add block value provider

* feat(plugin-workflow): improve todo block and drawer

* fix(plugin-workflow): fix instruction name in test cases

* fix(plugin-workflow): fix test cases

* refactor(plugin-workflow): change param type of collection field initializer

* feat(plugin-workflow): add filter types for getters

* fix(plugin-workflow): fix assignees variable

* fix(plugin-workflow): filter todo by exist workflow

* fix(plugin-workflow): fix duplicated save action in manual config

* fix(plugin-workflow): fix transaction

* feat(plugin-workflow): destroy workflow will be cascaded

* fix(plugin-workflow): fix merge

* fix(plugin-workflow): fix locale

* fix(plugin-workflow): allow open ui view when executed

* fix(plugin-workflow): change todo table filter

* feat(plugin-workflow): use formula for calculation

* fix(plugin-workflow): fix variable template regexp

* fix(plugin-workflow): fix sub-options logic with types

* refactor(plugin-workflow): drop useless component

* fix(plugin-workflow): fix manual node action button

* feat(plugin-workflow): add new variable input component

* refactor(plugin-workflow): change all variable to new component

* fix(plugin-workflow): fix type

* fix(plugin-workflow): fix functions init

* fix(plugin-workflow): change jsonb to json for stable order

* fix(plugin-workflow): fix duplicated field name when initialize

* fix(plugin-workflow): fix manual result in manual block

* test(plugin-workflow): log field initializer props

* fix(plugin-workflow): fix nullable arguments

* test(plugin-workflow): test initializer fields schema

* fix: observer

* fix(plugin-workflow): adjust hints

* fix(plugin-workflow): fix locale and cursor in variable input

* refactor(plugin-workflow): change status keys

* fix(plugin-workflow): fix parallel instruction

* feat: linkage rules

* refactor: linkage rules

* refactor: linkage rules update

* refactor: linkage rules button

* refactor(linakge rules): suport value action

* refactor(linakge rules): suport value action

* fix(workflow test): workflow test

* refactor(linakge rules): action linkage only for detail form

* refactor(linakge rules):  logic calculation

* refactor(linakge rules):  logic calculation

* refactor(linakge rules):  logic calculation

* refactor(linakge rules):  logic calculation

* refactor(linakge rules):  linkageAction

* refactor(linakge rules):  linkageAction

* refactor(linakge rules): field value  linkageAction

* refactor(linakge rules): local

* refactor(linakge rules): local

* fix(linakge rules): value property

* fix(linakge rules): value property

* fix(linakge rules): value property

* fix(linakge rules): conditionAnalyse

* fix(linakge rules): conditionAnalyse

* fix(linakge rules): conditionAnalyse

* fix(linakge rules): conditionAnalyse form fields

* fix(linakge rules): conditionAnalyse form fields

* fix(linakge rules): conditionAnalyse operators

* fix(linakge rules): form button linkage

* fix(linakge rules): form button linkage

* fix(linakge rules): select component suport pattern

* fix(linakge rules):  component suport field patter for disabled

* fix(linakge rules):  component suport field patter for disabled

* fix(linakge rules):  component suport field patter for disabled

* fix(linakge rules):  component suport field patter for disabled

* fix(action): action desginer suport linkage

* refactor: formula calculation assignment

* refactor: formula calculation assignment

* style: unified component disable style

* feat: richText suport disabled

* feat: condiction suport varible

* feat: condiction suport varible

* refactor: variableInput

* refactor: condiction scope

* refactor: multi level linkage

* fix: empry value linkage

* refactor: components using core schema-component

* fix: fix template reference linkage

* refactor: evalute foumala

* fix: mutual linkage fix

* refactor: variables suport obo&oho

* style: style improve

* style: style improve

* style: style improve

* feat: variables suport m2o

* refactor: code improve

* refactor: code improve

* refactor: code improve

* refactor: code improve

* refactor: code improve

* fix: actions path

* refactor: code improve

* refactor: code improve

* refactor: code improve

* refactor: code improve

* refactor: code improve

* refactor: code improve

* refactor: code improve

---------

Co-authored-by: mytharcher <mytharcher@gmail.com>
Co-authored-by: Junyi <mytharcher@users.noreply.github.com>
Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
katherinehhh 2023-02-25 00:40:51 +08:00 committed by GitHub
parent 8e11e4d561
commit 94e38e453a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 1968 additions and 355 deletions

View File

@ -41,6 +41,21 @@ export default {
"Collection category":"Collection category", "Collection category":"Collection category",
"Sort":"Sort", "Sort":"Sort",
"Categories":"Categories", "Categories":"Categories",
"Visible":"Visible",
"Read only":"Read only",
"Easy reading":"Easy reading",
"Hidden":"Hidden",
"Hidden(reserved value)":"Hidden(reserved value)",
"Not required":"Not required",
"Value":"Value",
"Disabled":"Disabled",
"Enabled":"Enabled",
"Linkage rule":"Linkage rule",
"Linkage rules":"Linkage rules",
"Condition":"Condition",
"Properties":"Properties",
"Add linkage rule":"Add linkage rule",
"Add property":"Add property",
"Category name":"Category name", "Category name":"Category name",
"Roles & Permissions": "Roles & Permissions", "Roles & Permissions": "Roles & Permissions",
"Edit profile": "Edit profile", "Edit profile": "Edit profile",

View File

@ -42,6 +42,21 @@ export default {
"Category name":"分類名", "Category name":"分類名",
"Delete category":"分類の削除", "Delete category":"分類の削除",
"Collection category":"Collection category", "Collection category":"Collection category",
"Visible":"表示",
"Read only":"読み取り専用(編集不可)",
"Easy reading":"読み取り専用(読取りモード)",
"Hidden":"非表示",
"Hidden(reserved value)":"非表示(値の保持)",
"Not required":"必須ではありません",
"Value":"フィールド値",
"Disabled":"無効化",
"Enabled":"有効化",
"Linkage rule":"連動規則",
"Linkage rules":"連動規則",
"Condition":"条件#ジョウケン#",
"Properties":"属性#ゾクセイ#",
"Add linkage rule":"連動規則の追加",
"Add property":"属性の追加",
"Roles & Permissions": "役割と権限", "Roles & Permissions": "役割と権限",
"Edit profile": "プロフィール", "Edit profile": "プロフィール",
"Change password": "パスワード変更", "Change password": "パスワード変更",
@ -377,7 +392,7 @@ export default {
"Role UID": "役割ID", "Role UID": "役割ID",
"Precision": "精度", "Precision": "精度",
"Formula mode": "数式モード", "Formula mode": "数式モード",
"Expression": "表", "Expression": "表达式",
"Input +, -, *, /, ( ) to calculate, input @ to open field variables.": "+、-、*、/、( ) で算術演算、@でフィールド変数を開くことができます。", "Input +, -, *, /, ( ) to calculate, input @ to open field variables.": "+、-、*、/、( ) で算術演算、@でフィールド変数を開くことができます。",
"Formula error.": "式の検証エラーです。", "Formula error.": "式の検証エラーです。",
"Accept": "ファイル形式", "Accept": "ファイル形式",

View File

@ -39,6 +39,22 @@ export default {
"Delete category":"删除分类", "Delete category":"删除分类",
"Edit category":"编辑分类", "Edit category":"编辑分类",
"Collection category":"数据表类别", "Collection category":"数据表类别",
"Visible":"显示",
"Read only":"只读(禁止编辑)",
"Easy reading":"只读(阅读模式)",
"Hidden":"隐藏",
"Hidden(reserved value)":"隐藏(保留值)",
"Not required":"非必填",
"Value":"字段值",
"Disabled":"禁用",
"Enabled":"启用",
"Linkage rule":"联动规则",
"Linkage rules":"联动规则",
"Condition":"条件",
"Properties":"属性",
"Add linkage rule":"添加联动规则",
"Add property":"添加属性",
"Expression":"表达式",
"Sort":"排序", "Sort":"排序",
"Categories":"数据表类别", "Categories":"数据表类别",
"Category name":"分类名称", "Category name":"分类名称",

View File

@ -3,8 +3,12 @@ import { isValid, uid } from '@formily/shared';
import { Menu, Select } from 'antd'; import { Menu, Select } from 'antd';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useActionContext, useCompile, useDesignable } from '../..'; import { useDesignable } from '../..';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'; import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { useCollection } from '../../../collection-manager';
import { useRecord } from '../../../record-provider';
import { useFormBlockContext } from '../../../block-provider/FormBlockProvider';
import { requestSettingsSchema } from './utils'; import { requestSettingsSchema } from './utils';
const MenuGroup = (props) => { const MenuGroup = (props) => {
@ -36,14 +40,14 @@ export const ActionDesigner = (props) => {
const { modalTip, ...restProps } = props; const { modalTip, ...restProps } = props;
const field = useField(); const field = useField();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { name } = useCollection();
const { dn } = useDesignable(); const { dn } = useDesignable();
const { t } = useTranslation(); const { t } = useTranslation();
const compile = useCompile();
const isPopupAction = ['create', 'update', 'view', 'customize:popup'].includes(fieldSchema['x-action'] || ''); const isPopupAction = ['create', 'update', 'view', 'customize:popup'].includes(fieldSchema['x-action'] || '');
const isUpdateModePopupAction = ['customize:bulkUpdate', 'customize:bulkEdit'].includes(fieldSchema['x-action']); const isUpdateModePopupAction = ['customize:bulkUpdate', 'customize:bulkEdit'].includes(fieldSchema['x-action']);
const context = useActionContext();
const [initialSchema, setInitialSchema] = useState<ISchema>(); const [initialSchema, setInitialSchema] = useState<ISchema>();
const actionType = fieldSchema['x-action'] ?? ''; const actionType = fieldSchema['x-action'] ?? '';
const isLinkageAction = Object.keys(useFormBlockContext()).length > 0 && Object.keys(useRecord()).length > 0;
useEffect(() => { useEffect(() => {
const schemaUid = uid(); const schemaUid = uid();
@ -132,6 +136,7 @@ export const ActionDesigner = (props) => {
} }
}} }}
/> />
{isLinkageAction && <SchemaSettings.LinkageRules collectionName={name} />}
{isPopupAction && ( {isPopupAction && (
<SchemaSettings.SelectItem <SchemaSettings.SelectItem
title={t('Open mode')} title={t('Open mode')}

View File

@ -2,12 +2,13 @@ import { css } from '@emotion/css';
import { observer, RecursionField, useField, useFieldSchema, useForm } from '@formily/react'; import { observer, RecursionField, useField, useFieldSchema, useForm } from '@formily/react';
import { Button, Modal, Popover } from 'antd'; import { Button, Modal, Popover } from 'antd';
import classnames from 'classnames'; import classnames from 'classnames';
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { useActionContext } from '../..'; import { useActionContext } from '../..';
import { Icon } from '../../../icon'; import { Icon } from '../../../icon';
import { SortableItem } from '../../common'; import { SortableItem } from '../../common';
import { useCompile, useDesigner } from '../../hooks'; import { useCompile, useDesigner } from '../../hooks';
import { useProps } from '../../hooks/useProps'; import { useProps } from '../../hooks/useProps';
import { useRecord } from '../../../record-provider';
import ActionContainer from './Action.Container'; import ActionContainer from './Action.Container';
import { ActionDesigner } from './Action.Designer'; import { ActionDesigner } from './Action.Designer';
import { ActionDrawer } from './Action.Drawer'; import { ActionDrawer } from './Action.Drawer';
@ -17,6 +18,8 @@ import { ActionPage } from './Action.Page';
import { ActionContext } from './context'; import { ActionContext } from './context';
import { useA } from './hooks'; import { useA } from './hooks';
import { ComposedAction } from './types'; import { ComposedAction } from './types';
import { useDesignable } from '../../';
import { linkageAction } from './utils';
export const actionDesignerCss = css` export const actionDesignerCss = css`
position: relative; position: relative;
@ -87,16 +90,27 @@ export const Action: ComposedAction = observer((props: any) => {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const compile = useCompile(); const compile = useCompile();
const form = useForm(); const form = useForm();
const values = useRecord();
const designerProps = fieldSchema['x-designer-props']; const designerProps = fieldSchema['x-designer-props'];
const openMode = fieldSchema?.['x-component-props']?.['openMode']; const openMode = fieldSchema?.['x-component-props']?.['openMode'];
const disabled = form.disabled || field.disabled; const disabled = form.disabled || field.disabled;
const openSize = fieldSchema?.['x-component-props']?.['openSize']; const openSize = fieldSchema?.['x-component-props']?.['openSize'];
const linkageRules = fieldSchema?.['x-linkage-rules'] || [];
const { designable } = useDesignable();
useEffect(() => {
linkageRules.map((v) => {
return v.actions?.map((h) => {
linkageAction(h.operator, field, v.condition, values, designable);
});
});
}, [linkageRules]);
const renderButton = () => ( const renderButton = () => (
<SortableItem <SortableItem
{...others} {...others}
loading={field?.data?.loading} loading={field?.data?.loading}
icon={<Icon type={icon} />} icon={<Icon type={icon} />}
disabled={disabled} disabled={disabled}
style={{ border: field?.data?.hidden && '1px dashed #ede9e9' }}
onClick={(e: React.MouseEvent) => { onClick={(e: React.MouseEvent) => {
if (!disabled) { if (!disabled) {
e.preventDefault(); e.preventDefault();

View File

@ -1,5 +1,7 @@
import type { ISchema } from '@formily/react'; import type { ISchema } from '@formily/react';
import { last } from 'lodash';
import { conditionAnalyse } from '../../common/utils/uitls';
import { ActionType } from '../../../schema-settings/LinkageRules/type';
const validateJSON = { const validateJSON = {
validator: `{{(value, rule)=> { validator: `{{(value, rule)=> {
if (!value) { if (!value) {
@ -66,3 +68,41 @@ export const requestSettingsSchema: ISchema = {
}, },
}, },
}; };
export const linkageAction = (operator, field, condition, values, designable) => {
const displayResult = [field.display];
const disableResult = [field.disabled];
switch (operator) {
case ActionType.Visible:
if (conditionAnalyse(condition, values)) {
displayResult.push(operator);
}
field.display = last(displayResult);
break;
case ActionType.Hidden:
if (conditionAnalyse(condition, values)) {
if (!designable) {
displayResult.push(operator);
} else {
field.data = field.data || {};
field.data.hidden = true;
}
}
field.display = last(displayResult);
break;
case ActionType.Disabled:
if (conditionAnalyse(condition, values)) {
disableResult.push(true);
}
field.disabled = last(disableResult);
break;
case ActionType.Active:
if (conditionAnalyse(condition, values)) {
disableResult.push(false);
}
field.disabled = last(disableResult);
break;
default:
return null;
}
};

View File

@ -23,12 +23,6 @@ export const Checkbox: ComposedCheckbox = connect(
value: 'checked', value: 'checked',
onInput: 'onChange', onInput: 'onChange',
}, },
(props, field) => {
// console.log({ props, field });
return {
...props,
};
},
), ),
mapReadPretty((props) => { mapReadPretty((props) => {
if (!isValid(props.value)) { if (!isValid(props.value)) {
@ -45,9 +39,11 @@ Checkbox.__ANT_CHECKBOX = true;
Checkbox.Group = connect( Checkbox.Group = connect(
AntdCheckbox.Group, AntdCheckbox.Group,
mapProps({ mapProps(
{
dataSource: 'options', dataSource: 'options',
}), },
),
mapReadPretty((props) => { mapReadPretty((props) => {
if (!isValid(props.value)) { if (!isValid(props.value)) {
return null; return null;

View File

@ -47,7 +47,7 @@ export const moment2str = (value?: moment.Moment | moment.Moment[], options: Mom
}; };
export const mapDateFormat = function () { export const mapDateFormat = function () {
return (props: any) => { return (props: any, field) => {
const format = getDefaultFormat(props) as any; const format = getDefaultFormat(props) as any;
const onChange = props.onChange; const onChange = props.onChange;
return { return {

View File

@ -35,6 +35,9 @@ export const FilterItem = observer((props: any) => {
placeholder={t('Select Field')} placeholder={t('Select Field')}
/> />
<Select <Select
className={css`
min-width: 110px;
`}
value={operator?.value} value={operator?.value}
options={compile(operators)} options={compile(operators)}
onChange={(value) => { onChange={(value) => {

View File

@ -26,6 +26,7 @@ export const FormDesigner = () => {
{/* <SchemaSettings.Template componentName={'FormItem'} collectionName={name} /> */} {/* <SchemaSettings.Template componentName={'FormItem'} collectionName={name} /> */}
<SchemaSettings.BlockTitleItem /> <SchemaSettings.BlockTitleItem />
<SchemaSettings.FormItemTemplate componentName={'FormItem'} collectionName={name} resourceName={defaultResource} /> <SchemaSettings.FormItemTemplate componentName={'FormItem'} collectionName={name} resourceName={defaultResource} />
<SchemaSettings.LinkageRules collectionName={name} />
<SchemaSettings.Divider /> <SchemaSettings.Divider />
<SchemaSettings.Remove <SchemaSettings.Remove
removeParentsIfNoChildren removeParentsIfNoChildren

View File

@ -1,5 +1,5 @@
import { FormLayout } from '@formily/antd'; import { FormLayout } from '@formily/antd';
import { createForm, Field, onFormInputChange } from '@formily/core'; import { createForm, Field, onFormInputChange, onFieldReact, onFieldInit, onFieldChange } from '@formily/core';
import { FieldContext, FormContext, observer, RecursionField, useField, useFieldSchema } from '@formily/react'; import { FieldContext, FormContext, observer, RecursionField, useField, useFieldSchema } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { ConfigProvider, Spin } from 'antd'; import { ConfigProvider, Spin } from 'antd';
@ -7,6 +7,7 @@ import React, { useEffect, useMemo } from 'react';
import { useActionContext } from '..'; import { useActionContext } from '..';
import { useAttach, useComponent } from '../..'; import { useAttach, useComponent } from '../..';
import { useProps } from '../../hooks/useProps'; import { useProps } from '../../hooks/useProps';
import { linkageMergeAction } from './utils';
export interface FormProps { export interface FormProps {
[key: string]: any; [key: string]: any;
@ -54,10 +55,21 @@ const FormDecorator: React.FC<FormProps> = (props) => {
); );
}; };
const getLinkageRules = (fieldSchema) => {
let linkageRules = null;
fieldSchema.mapProperties((schema) => {
if (schema['x-linkage-rules']) {
linkageRules = schema['x-linkage-rules'];
}
});
return linkageRules;
};
const WithForm = (props) => { const WithForm = (props) => {
const { form } = props; const { form } = props;
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { setFormValueChanged } = useActionContext(); const { setFormValueChanged } = useActionContext();
const linkageRules = getLinkageRules(fieldSchema) || fieldSchema.parent?.['x-linkage-rules'] || [];
useEffect(() => { useEffect(() => {
const id = uid(); const id = uid();
form.addEffects(id, () => { form.addEffects(id, () => {
@ -70,6 +82,59 @@ const WithForm = (props) => {
form.removeEffects(id); form.removeEffects(id);
}; };
}, []); }, []);
useEffect(() => {
const id = uid();
form.addEffects(id, () => {
return linkageRules.map((v) => {
return v.actions?.map((h) => {
if (h.targetFields) {
const fields = h.targetFields.join(',');
onFieldInit(`*(${fields})`, (field: any, form) => {
field['initProperty'] = {
display: field.display,
required: field.required,
pattern: field.pattern,
value: field.value || field.initialValue,
};
});
onFieldChange(`*(${fields})`, ['value', 'required', 'pattern', 'display'], (field: any) => {
field.linkageProperty = {};
});
}
});
});
});
return () => {
form.removeEffects(id);
};
}, []);
useEffect(() => {
const id = uid();
form.addEffects(id, () => {
const linkagefields = [];
return linkageRules.map((v, index) => {
return v.actions?.map((h) => {
if (h.targetFields) {
const fields = h.targetFields.join(',');
return onFieldReact(`*(${fields})`, (field: any, form) => {
linkagefields.push(field);
linkageMergeAction(h, field, v.condition, form?.values);
if (index === linkageRules.length - 1) {
setTimeout(() =>
linkagefields.map((v) => {
v.linkageProperty = {};
}),
);
}
});
}
});
});
});
return () => {
form.removeEffects(id);
};
}, [linkageRules]);
return fieldSchema['x-decorator'] === 'Form' ? <FormDecorator {...props} /> : <FormComponent {...props} />; return fieldSchema['x-decorator'] === 'Form' ? <FormDecorator {...props} /> : <FormComponent {...props} />;
}; };

View File

@ -0,0 +1,109 @@
import { last, get } from 'lodash';
import * as functions from '@formulajs/formulajs';
import { conditionAnalyse } from '../../common/utils/uitls';
import { ActionType } from '../../../schema-settings/LinkageRules/type';
function now() {
return new Date();
}
const fnNames = Object.keys(functions).filter((key) => key !== 'default');
const fns = fnNames.map((key) => functions[key]);
function formula(expression: string, scope = {}) {
try {
const fn = new Function(...fnNames, ...Object.keys(scope), `return ${expression}`);
const result = fn(...fns, ...Object.values(scope));
if (typeof result === 'number') {
if (Number.isNaN(result) || !Number.isFinite(result)) {
return null;
}
return functions.ROUND(result, 9);
}
if (typeof result === 'function') {
return result();
}
return result;
} catch (error) {
return undefined;
}
}
export function evaluate(expression: string, scope = {}) {
const mergeScope = { ...scope, now };
const exp = expression.trim().replace(/{{\s*([^{}]+)\s*}}/g, (_, v) => {
const item: any = get(scope, v);
const key = v.replace(/\.(\d+)/g, '["$1"]');
return ` ${typeof item === 'function' ? item() : key} `;
});
return formula(exp, mergeScope);
}
export const linkageMergeAction = ({ operator, value }, field, condition, values) => {
const requiredResult = field?.linkageProperty?.required || [field?.initProperty?.required || false];
const displayResult = field?.linkageProperty?.display || [field?.initProperty?.display];
const patternResult = field?.linkageProperty?.pattern || [field?.initProperty?.pattern];
const valueResult = field?.linkageProperty?.value || [field.value || field?.initProperty?.value];
switch (operator) {
case ActionType.Required:
if (conditionAnalyse(condition, values)) {
requiredResult.push(true);
}
field.linkageProperty = {
...field.linkageProperty,
required: requiredResult,
};
field.required = last(field.linkageProperty?.required);
break;
case ActionType.InRequired:
if (conditionAnalyse(condition, values)) {
requiredResult.push(false);
}
field.linkageProperty = {
...field.linkageProperty,
required: requiredResult,
};
field.required = last(field.linkageProperty?.required);
break;
case ActionType.Visible:
case ActionType.None:
case ActionType.Hidden:
if (conditionAnalyse(condition, values)) {
displayResult.push(operator);
}
field.linkageProperty = {
...field.linkageProperty,
display: displayResult,
};
field.display = last(field.linkageProperty?.display);
break;
case ActionType.Editable:
case ActionType.ReadOnly:
case ActionType.ReadPretty:
if (conditionAnalyse(condition, values)) {
patternResult.push(operator);
}
field.linkageProperty = {
...field.linkageProperty,
pattern: patternResult,
};
field.pattern = last(field.linkageProperty.pattern);
break;
case ActionType.Value:
if (conditionAnalyse(condition, values)) {
if (value?.mode === 'express') {
const result = evaluate(value.result || value.value, values);
valueResult.push(result);
} else {
valueResult.push(value?.value || value);
}
}
field.linkageProperty = {
...field.linkageProperty,
value: valueResult,
};
setTimeout(() => (field.value = last(valueResult) === undefined ? field.value : last(valueResult)));
break;
default:
return null;
}
};

View File

@ -1,14 +1,15 @@
import { connect, mapReadPretty } from '@formily/react'; import { connect, mapReadPretty, useField } from '@formily/react';
import { InputNumber as AntdNumber } from 'antd'; import { InputNumber as AntdNumber } from 'antd';
import React from 'react'; import React from 'react';
import { ReadPretty } from './ReadPretty'; import { ReadPretty } from './ReadPretty';
export const InputNumber = connect((props) => { export const InputNumber = connect((props) => {
const { onChange, ...others } = props; const { onChange, ...others } = props;
const field = useField<any>();
const handleChange = (v) => { const handleChange = (v) => {
onChange(parseFloat(v)); onChange(parseFloat(v));
} };
return (<AntdNumber onChange={handleChange} {...others} />); return <AntdNumber onChange={handleChange} {...others} />;
}, mapReadPretty(ReadPretty)); }, mapReadPretty(ReadPretty));
export default InputNumber; export default InputNumber;

View File

@ -39,6 +39,9 @@ Input.TextArea = connect(
Input.URL = connect(AntdInput, mapReadPretty(ReadPretty.URL)); Input.URL = connect(AntdInput, mapReadPretty(ReadPretty.URL));
Input.JSON = connect(Json, mapReadPretty(ReadPretty.JSON)); Input.JSON = connect(
Json,
mapReadPretty(ReadPretty.JSON),
);
export default Input; export default Input;

View File

@ -1,4 +1,4 @@
import { connect, mapReadPretty } from '@formily/react'; import { connect, mapReadPretty,mapProps } from '@formily/react';
import { isValid } from '@formily/shared'; import { isValid } from '@formily/shared';
import { Input } from 'antd'; import { Input } from 'antd';
import { PasswordProps } from 'antd/lib/input'; import { PasswordProps } from 'antd/lib/input';

View File

@ -1,4 +1,4 @@
import { connect, mapReadPretty } from '@formily/react'; import { connect, mapReadPretty,mapProps } from '@formily/react';
import { InputNumber } from 'antd'; import { InputNumber } from 'antd';
import React from 'react'; import React from 'react';
import { ReadPretty } from '../input-number/ReadPretty'; import { ReadPretty } from '../input-number/ReadPretty';

View File

@ -11,18 +11,22 @@ type ComposedRadio = React.FC<RadioProps> & {
export const Radio: ComposedRadio = connect( export const Radio: ComposedRadio = connect(
AntdRadio, AntdRadio,
mapProps({ mapProps(
{
value: 'checked', value: 'checked',
onInput: 'onChange', onInput: 'onChange',
}), },
),
); );
Radio.__ANT_RADIO = true; Radio.__ANT_RADIO = true;
Radio.Group = connect( Radio.Group = connect(
AntdRadio.Group, AntdRadio.Group,
mapProps({ mapProps(
{
dataSource: 'options', dataSource: 'options',
}), },
),
mapReadPretty((props) => { mapReadPretty((props) => {
if (!isValid(props.value)) { if (!isValid(props.value)) {
return <div></div>; return <div></div>;

View File

@ -1,9 +1,8 @@
import { connect, mapReadPretty } from '@formily/react'; import { connect, mapProps, mapReadPretty } from '@formily/react';
import { InputRecordPicker } from './InputRecordPicker'; import { InputRecordPicker } from './InputRecordPicker';
import { ReadPrettyRecordPicker } from './ReadPrettyRecordPicker'; import { ReadPrettyRecordPicker } from './ReadPrettyRecordPicker';
export const RecordPicker: any = connect( export const RecordPicker: any = connect(
InputRecordPicker, InputRecordPicker,
// mapProps(mapSuffixProps),
mapReadPretty(ReadPrettyRecordPicker), mapReadPretty(ReadPrettyRecordPicker),
); );

View File

@ -1,4 +1,4 @@
import { connect, mapReadPretty } from '@formily/react'; import { connect, mapReadPretty, mapProps } from '@formily/react';
import React from 'react'; import React from 'react';
import ReactQuill from 'react-quill'; import ReactQuill from 'react-quill';
import { ReadPretty as InputReadPretty } from '../input'; import { ReadPretty as InputReadPretty } from '../input';
@ -7,11 +7,7 @@ import './style.less';
export const RichText = connect( export const RichText = connect(
(props) => { (props) => {
const modules = { const modules = {
toolbar: [ toolbar: [['bold', 'italic', 'underline', 'link'], [{ list: 'ordered' }, { list: 'bullet' }], ['clean']],
['bold', 'italic', 'underline', 'link'],
[{ list: 'ordered' }, { list: 'bullet' }],
['clean'],
],
}; };
const formats = [ const formats = [
'header', 'header',
@ -26,13 +22,14 @@ export const RichText = connect(
'link', 'link',
'image', 'image',
]; ];
const { value, onChange } = props; const { value, onChange, disabled } = props;
return ( return (
<ReactQuill <ReactQuill
modules={modules} modules={modules}
formats={formats} formats={formats}
value={typeof value === 'string' ? value : ''} value={typeof value === 'string' ? value : ''}
onChange={onChange} onChange={onChange}
readOnly={disabled}
/> />
); );
}, },

View File

@ -10,7 +10,7 @@ type ComposedTimePicker = React.FC<AntdTimePickerProps> & {
}; };
const mapTimeFormat = function () { const mapTimeFormat = function () {
return (props: any) => { return (props: any, field) => {
const format = props['format'] || 'HH:mm:ss'; const format = props['format'] || 'HH:mm:ss';
const onChange = props.onChange; const onChange = props.onChange;
return { return {

View File

@ -25,7 +25,8 @@ export const Upload: ComposedUpload = connect(
mapReadPretty(ReadPretty.Upload), mapReadPretty(ReadPretty.Upload),
); );
Upload.Attachment = connect((props: UploadProps) => { Upload.Attachment = connect(
(props: UploadProps) => {
const { disabled, multiple, value, onChange } = props; const { disabled, multiple, value, onChange } = props;
const [fileList, setFileList] = useState([]); const [fileList, setFileList] = useState([]);
const [sync, setSync] = useState(true); const [sync, setSync] = useState(true);
@ -191,7 +192,9 @@ Upload.Attachment = connect((props: UploadProps) => {
)} )}
</div> </div>
); );
}, mapReadPretty(ReadPretty.Attachment)); },
mapReadPretty(ReadPretty.Attachment),
);
Upload.Dragger = connect( Upload.Dragger = connect(
(props: DraggerProps) => { (props: DraggerProps) => {

View File

@ -1,15 +1,13 @@
import React from "react"; import React from 'react';
import { useForm } from '@formily/react'; import { useForm } from '@formily/react';
import { Cascader, Input as AntInput, Button, Tag, InputNumber, Select, DatePicker } from "antd"; import { Cascader, Input as AntInput, Button, Tag, InputNumber, Select, DatePicker } from 'antd';
import { CloseCircleFilled } from "@ant-design/icons"; import { CloseCircleFilled } from '@ant-design/icons';
import { cx, css } from "@emotion/css"; import { cx, css } from '@emotion/css';
import { useTranslation } from "react-i18next"; import { useTranslation } from 'react-i18next';
import moment from "moment"; import moment from 'moment';
import { useCompile } from '../../hooks/useCompile'; import { useCompile } from '../../hooks/useCompile';
const JT_VALUE_RE = /^\s*{{\s*([^{}]+)\s*}}\s*$/; const JT_VALUE_RE = /^\s*{{\s*([^{}]+)\s*}}\s*$/;
function parseValue(value: any): string | string[] { function parseValue(value: any): string | string[] {
@ -37,27 +35,17 @@ const ConstantTypes = {
label: `{{t("String")}}`, label: `{{t("String")}}`,
value: 'string', value: 'string',
component({ onChange, value }) { component({ onChange, value }) {
return ( return <AntInput value={value} onChange={(ev) => onChange(ev.target.value)} />;
<AntInput
value={value}
onChange={ev => onChange(ev.target.value)}
/>
);
}, },
default: '' default: '',
}, },
number: { number: {
label: '{{t("Number")}}', label: '{{t("Number")}}',
value: 'number', value: 'number',
component({ onChange, value }) { component({ onChange, value }) {
return ( return <InputNumber value={value} onChange={onChange} />;
<InputNumber
value={value}
onChange={onChange}
/>
);
}, },
default: 0 default: 0,
}, },
boolean: { boolean: {
label: `{{t("Boolean")}}`, label: `{{t("Boolean")}}`,
@ -76,7 +64,7 @@ const ConstantTypes = {
/> />
); );
}, },
default: false default: false,
}, },
date: { date: {
label: '{{t("Date")}}', label: '{{t("Date")}}',
@ -85,7 +73,7 @@ const ConstantTypes = {
return ( return (
<DatePicker <DatePicker
value={moment(value)} value={moment(value)}
onChange={(d) => d ? onChange(d.toDate()) : null} onChange={(d) => (d ? onChange(d.toDate()) : null)}
allowClear={false} allowClear={false}
showTime showTime
/> />
@ -101,9 +89,7 @@ const ConstantTypes = {
value: 'null', value: 'null',
component() { component() {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return <AntInput readOnly placeholder={t('Null')} className="null-value" />;
<AntInput readOnly placeholder={t('Null')} className="null-value" />
);
}, },
default: null, default: null,
}, },
@ -113,7 +99,7 @@ type VariableOptions = {
value: string; value: string;
label?: string; label?: string;
children?: VariableOptions[]; children?: VariableOptions[];
} };
export function Input(props) { export function Input(props) {
const { value = '', scope, onChange, children, button } = props; const { value = '', scope, onChange, children, button } = props;
@ -127,7 +113,7 @@ export function Input(props) {
const { t } = useTranslation(); const { t } = useTranslation();
const options: VariableOptions[] = compile([ const options: VariableOptions[] = compile([
{ value: '', label: t('Constant'), children: children ? null : constantOptions }, { value: '', label: t('Constant'), children: children ? null : constantOptions },
...(typeof scope === 'function' ? scope() : (scope ?? [])) ...(typeof scope === 'function' ? scope() : scope ?? []),
]); ]);
const form = useForm(); const form = useForm();
@ -148,16 +134,22 @@ export function Input(props) {
onChange(`{{${next.join('.')}}}`); onChange(`{{${next.join('.')}}}`);
} }
const variableText = variable?.reduce((opts, key, i) => { const variableText = variable
const option = (i ? (opts[i - 1] as VariableOptions).children : options)?.find(item => item.value === key); ?.reduce((opts, key, i) => {
const option = (i ? (opts[i - 1] as VariableOptions).children : options)?.find((item) => item.value === key);
return option ? opts.concat(option) : opts; return option ? opts.concat(option) : opts;
}, [] as VariableOptions[]).map(item => item.label).join(' / '); }, [] as VariableOptions[])
.map((item) => item.label)
.join(' / ');
const disabled = props.disabled || form.disabled; const disabled = props.disabled || form.disabled;
return ( return (
<AntInput.Group compact className={css` <AntInput.Group
compact
className={css`
width: auto; width: auto;
display: flex !important;
.ant-input-disabled { .ant-input-disabled {
.ant-tag { .ant-tag {
color: #bfbfbf; color: #bfbfbf;
@ -170,15 +162,15 @@ export function Input(props) {
} }
`} `}
> >
{variable {variable ? (
? ( <div
<div className={css` className={css`
position: relative; position: relative;
line-height: 0; line-height: 0;
&:hover { &:hover {
.ant-select-clear { .ant-select-clear {
opacity: .8; opacity: 0.8;
} }
} }
@ -195,9 +187,10 @@ export function Input(props) {
border-radius: 10px; border-radius: 10px;
} }
} }
`}> `}
>
<div <div
onInput={e => e.preventDefault()} onInput={(e) => e.preventDefault()}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key !== 'Backspace') { if (e.key !== 'Backspace') {
e.preventDefault(); e.preventDefault();
@ -209,50 +202,44 @@ export function Input(props) {
contentEditable={!disabled} contentEditable={!disabled}
suppressContentEditableWarning suppressContentEditableWarning
> >
<Tag contentEditable={false} color="blue">{variableText}</Tag> <Tag contentEditable={false} color="blue">
{variableText}
</Tag>
</div> </div>
{!disabled {!disabled ? (
? (
<span <span
className={cx('ant-select-clear', css` className={cx(
user-select: 'none' 'ant-select-clear',
`)} css`
user-select: 'none';
`,
)}
unselectable="on" unselectable="on"
aria-hidden aria-hidden
onClick={() => onChange(null)} onClick={() => onChange(null)}
> >
<CloseCircleFilled /> <CloseCircleFilled />
</span> </span>
) ) : null}
: null}
</div> </div>
) ) : (
: (
children ?? <ConstantComponent value={value} onChange={onChange} /> children ?? <ConstantComponent value={value} onChange={onChange} />
) )}
} {options.length > 1 ? (
{options.length > 1 <Cascader value={variable ?? ['', ...(children ? [] : [type])]} options={options} onChange={onSwitch}>
? (
<Cascader
value={variable ?? ['', ...(children ? [] : [type])]}
options={options}
onChange={onSwitch}
>
{button ?? ( {button ?? (
<Button <Button
type={variable ? 'primary' : 'default'} type={variable ? 'primary' : 'default'}
className={css` className={css`
font-style: italic; font-style: italic;
font-family: "New York", "Times New Roman", Times, serif; font-family: 'New York', 'Times New Roman', Times, serif;
`} `}
> >
x x
</Button> </Button>
)} )}
</Cascader> </Cascader>
) ) : null}
: null
}
</AntInput.Group> </AntInput.Group>
); );
} }

View File

@ -1,11 +1,9 @@
import React, { useState, useEffect, useRef, useMemo } from 'react'; import React, { useState, useEffect, useRef, useMemo } from 'react';
import { Input, Cascader, Tooltip, Button } from 'antd'; import { Input, Cascader, Tooltip, Button } from 'antd';
import { useForm } from '@formily/react'; import { useForm } from '@formily/react';
import { cx, css } from "@emotion/css"; import { cx, css } from '@emotion/css';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const VARIABLE_RE = /{{\s*([^{}]+)\s*}}/g; const VARIABLE_RE = /{{\s*([^{}]+)\s*}}/g;
function pasteHtml(container, html, { selectPastedContent = false, range: indexes }) { function pasteHtml(container, html, { selectPastedContent = false, range: indexes }) {
@ -100,12 +98,14 @@ function createOptionsValueLabelMap(options: any[]) {
function createVariableTagHTML(variable, keyLabelMap) { function createVariableTagHTML(variable, keyLabelMap) {
const labels = keyLabelMap.get(variable); const labels = keyLabelMap.get(variable);
return `<span class="ant-tag ant-tag-blue" contentEditable="false" data-key="${variable}">${labels?.join(' / ')}</span>`; return `<span class="ant-tag ant-tag-blue" contentEditable="false" data-key="${variable}">${labels?.join(
' / ',
)}</span>`;
} }
export function TextArea(props) { export function TextArea(props) {
const { value = '', scope, onChange, multiline = true, button } = props; const { value = '', scope, onChange, multiline = true, button } = props;
const { t } = useTranslation() const { t } = useTranslation();
const inputRef = useRef<HTMLDivElement>(null); const inputRef = useRef<HTMLDivElement>(null);
const options = (typeof scope === 'function' ? scope() : scope) ?? []; const options = (typeof scope === 'function' ? scope() : scope) ?? [];
const form = useForm(); const form = useForm();
@ -140,7 +140,7 @@ export function TextArea(props) {
}, [html]); }, [html]);
function onInsert(keyPath) { function onInsert(keyPath) {
const variable: string[] = keyPath.filter(key => Boolean(key.trim())); const variable: string[] = keyPath.filter((key) => Boolean(key.trim()));
const { current } = inputRef; const { current } = inputRef;
if (!current || !variable) { if (!current || !variable) {
return; return;
@ -176,11 +176,14 @@ export function TextArea(props) {
const disabled = props.disabled || form.disabled; const disabled = props.disabled || form.disabled;
return ( return (
<Input.Group compact className={css` <Input.Group
compact
className={css`
&.ant-input-group.ant-input-group-compact { &.ant-input-group.ant-input-group-compact {
display: flex; display: flex;
.ant-input { .ant-input {
flex-grow: 1; flex-grow: 1;
min-width: 200px;
} }
.ant-input-disabled { .ant-input-disabled {
.ant-tag { .ant-tag {
@ -189,7 +192,8 @@ export function TextArea(props) {
} }
} }
} }
`}> `}
>
<div <div
onInput={onInput} onInput={onInput}
onBlur={onBlur} onBlur={onBlur}
@ -198,33 +202,33 @@ export function TextArea(props) {
e.preventDefault(); e.preventDefault();
} }
}} }}
className={cx('ant-input', { 'ant-input-disabled': disabled }, css` className={cx(
'ant-input',
{ 'ant-input-disabled': disabled },
css`
overflow: auto; overflow: auto;
white-space: ${multiline ? 'normal' : 'nowrap'}; white-space: ${multiline ? 'normal' : 'nowrap'};
.ant-tag { .ant-tag {
display: inline; display: inline;
line-height: 19px; line-height: 19px;
margin: 0 .5em; margin: 0 0.5em;
padding: 2px 7px; padding: 2px 7px;
border-radius: 10px; border-radius: 10px;
} }
`)} `,
)}
ref={inputRef} ref={inputRef}
contentEditable={!disabled} contentEditable={!disabled}
dangerouslySetInnerHTML={{ __html: html }} dangerouslySetInnerHTML={{ __html: html }}
/> />
<Tooltip title={t('Use variable')}> <Tooltip title={t('Use variable')}>
<Cascader <Cascader value={[]} options={options} onChange={onInsert}>
value={[]}
options={options}
onChange={onInsert}
>
{button ?? ( {button ?? (
<Button <Button
className={css` className={css`
font-style: italic; font-style: italic;
font-family: "New York", "Times New Roman", Times, serif; font-family: 'New York', 'Times New Roman', Times, serif;
`} `}
> >
x x

View File

@ -0,0 +1,487 @@
/* globals define,module */
/*
Using a Universal Module Loader that should be browser, require, and AMD friendly
http://ricostacruz.com/cheatsheets/umdjs.html
*/
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define(factory);
} else if (typeof exports === 'object') {
module.exports = factory();
} else {
root.jsonLogic = factory();
}
})(this, function () {
'use strict';
/* globals console:false */
if (!Array.isArray) {
Array.isArray = function (arg) {
return Object.prototype.toString.call(arg) === '[object Array]';
};
}
/**
* Return an array that contains no duplicates (original not modified)
* @param {array} array Original reference array
* @return {array} New array with no duplicates
*/
function arrayUnique(array) {
var a = [];
for (var i = 0, l = array.length; i < l; i++) {
if (a.indexOf(array[i]) === -1) {
a.push(array[i]);
}
}
return a;
}
var jsonLogic = {};
var operations = {
$is: function (a, b) {
return a == b;
},
$eq: function (a, b) {
return a === b;
},
$ne: function (a, b) {
return a != b;
},
'!==': function (a, b) {
return a !== b;
},
$gt: function (a, b) {
return a > b;
},
$gte: function (a, b) {
return a >= b;
},
$lt: function (a, b, c) {
return c === undefined ? a < b : a < b && b < c;
},
$lte: function (a, b, c) {
return c === undefined ? a <= b : a <= b && b <= c;
},
$notEmpty: function (a) {
return jsonLogic.truthy(a);
},
$empty: function (a) {
return !jsonLogic.truthy(a);
},
'%': function (a, b) {
return a % b;
},
log: function (a) {
console.log(a);
return a;
},
$in: function (a, b) {
if (!b || typeof b.indexOf === 'undefined') return false;
return b.indexOf(a) !== -1;
},
$notIn: function (a, b) {
if (!b || typeof b.indexOf === 'undefined') return false;
return !(b.indexOf(a) !== -1);
},
$includes: function (a, b) {
if (!a || typeof a.indexOf === 'undefined') return false;
return a.indexOf(b) !== -1;
},
$notIncludes:function (a, b) {
if (!a || typeof a.indexOf === 'undefined') return false;
return !(a.indexOf(b) !== -1);
},
cat: function () {
return Array.prototype.join.call(arguments, '');
},
substr: function (source, start, end) {
if (end < 0) {
// JavaScript doesn't support negative end, this emulates PHP behavior
var temp = String(source).substr(start);
return temp.substr(0, temp.length + end);
}
return String(source).substr(start, end);
},
'+': function () {
return Array.prototype.reduce.call(
arguments,
function (a, b) {
return parseFloat(a, 10) + parseFloat(b, 10);
},
0,
);
},
'*': function () {
return Array.prototype.reduce.call(arguments, function (a, b) {
return parseFloat(a, 10) * parseFloat(b, 10);
});
},
'-': function (a, b) {
if (b === undefined) {
return -a;
} else {
return a - b;
}
},
'/': function (a, b) {
return a / b;
},
min: function () {
return Math.min.apply(this, arguments);
},
max: function () {
return Math.max.apply(this, arguments);
},
merge: function () {
return Array.prototype.reduce.call(
arguments,
function (a, b) {
return a.concat(b);
},
[],
);
},
var: function (a, b) {
var not_found = b === undefined ? null : b;
var data = this;
if (typeof a === 'undefined' || a === '' || a === null) {
return data;
}
var sub_props = String(a).split('.');
for (var i = 0; i < sub_props.length; i++) {
if (data === null || data === undefined) {
return not_found;
}
// Descending into data
data = data[sub_props[i]];
if (data === undefined) {
return not_found;
}
}
return data;
},
missing: function () {
/*
Missing can receive many keys as many arguments, like {"missing:[1,2]}
Missing can also receive *one* argument that is an array of keys,
which typically happens if it's actually acting on the output of another command
(like 'if' or 'merge')
*/
var missing = [];
var keys = Array.isArray(arguments[0]) ? arguments[0] : arguments;
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var value = jsonLogic.apply({ var: key }, this);
if (value === null || value === '') {
missing.push(key);
}
}
return missing;
},
missing_some: function (need_count, options) {
// missing_some takes two arguments, how many (minimum) items must be present, and an array of keys (just like 'missing') to check for presence.
var are_missing = jsonLogic.apply({ missing: options }, this);
if (options.length - are_missing.length >= need_count) {
return [];
} else {
return are_missing;
}
},
};
jsonLogic.is_logic = function (logic) {
return (
typeof logic === 'object' && // An object
logic !== null && // but not null
!Array.isArray(logic) && // and not an array
Object.keys(logic).length === 1 // with exactly one key
);
};
/*
This helper will defer to the JsonLogic spec as a tie-breaker when different language interpreters define different behavior for the truthiness of primitives. E.g., PHP considers empty arrays to be falsy, but Javascript considers them to be truthy. JsonLogic, as an ecosystem, needs one consistent answer.
Spec and rationale here: http://jsonlogic.com/truthy
*/
jsonLogic.truthy = function (value) {
if (Array.isArray(value) && value.length === 0) {
return false;
}
return !!value;
};
jsonLogic.get_operator = function (logic) {
return Object.keys(logic)[0];
};
jsonLogic.get_values = function (logic) {
return logic[jsonLogic.get_operator(logic)];
};
jsonLogic.apply = function (logic, data) {
// Does this array contain logic? Only one way to find out.
if (Array.isArray(logic)) {
return logic.map(function (l) {
return jsonLogic.apply(l, data);
});
}
// You've recursed to a primitive, stop!
if (!jsonLogic.is_logic(logic)) {
return logic;
}
var op = jsonLogic.get_operator(logic);
var values = logic[op];
var i;
var current;
var scopedLogic;
var scopedData;
var initial;
// easy syntax for unary operators, like {"var" : "x"} instead of strict {"var" : ["x"]}
if (!Array.isArray(values)) {
values = [values];
}
// 'if', 'and', and 'or' violate the normal rule of depth-first calculating consequents, let each manage recursion as needed.
if (op === 'if' || op == '?:') {
/* 'if' should be called with a odd number of parameters, 3 or greater
This works on the pattern:
if( 0 ){ 1 }else{ 2 };
if( 0 ){ 1 }else if( 2 ){ 3 }else{ 4 };
if( 0 ){ 1 }else if( 2 ){ 3 }else if( 4 ){ 5 }else{ 6 };
The implementation is:
For pairs of values (0,1 then 2,3 then 4,5 etc)
If the first evaluates truthy, evaluate and return the second
If the first evaluates falsy, jump to the next pair (e.g, 0,1 to 2,3)
given one parameter, evaluate and return it. (it's an Else and all the If/ElseIf were false)
given 0 parameters, return NULL (not great practice, but there was no Else)
*/
for (i = 0; i < values.length - 1; i += 2) {
if (jsonLogic.truthy(jsonLogic.apply(values[i], data))) {
return jsonLogic.apply(values[i + 1], data);
}
}
if (values.length === i + 1) {
return jsonLogic.apply(values[i], data);
}
return null;
} else if (op === '$and') {
// Return first falsy, or last
for (i = 0; i < values.length; i += 1) {
current = jsonLogic.apply(values[i], data);
if (!jsonLogic.truthy(current)) {
return current;
}
}
return current; // Last
} else if (op === 'or') {
// Return first truthy, or last
for (i = 0; i < values.length; i += 1) {
current = jsonLogic.apply(values[i], data);
if (jsonLogic.truthy(current)) {
return current;
}
}
return current; // Last
} else if (op === 'filter') {
scopedData = jsonLogic.apply(values[0], data);
scopedLogic = values[1];
if (!Array.isArray(scopedData)) {
return [];
}
// Return only the elements from the array in the first argument,
// that return truthy when passed to the logic in the second argument.
// For parity with JavaScript, reindex the returned array
return scopedData.filter(function (datum) {
return jsonLogic.truthy(jsonLogic.apply(scopedLogic, datum));
});
} else if (op === 'map') {
scopedData = jsonLogic.apply(values[0], data);
scopedLogic = values[1];
if (!Array.isArray(scopedData)) {
return [];
}
return scopedData.map(function (datum) {
return jsonLogic.apply(scopedLogic, datum);
});
} else if (op === 'reduce') {
scopedData = jsonLogic.apply(values[0], data);
scopedLogic = values[1];
initial = typeof values[2] !== 'undefined' ? values[2] : null;
if (!Array.isArray(scopedData)) {
return initial;
}
return scopedData.reduce(function (accumulator, current) {
return jsonLogic.apply(scopedLogic, { current: current, accumulator: accumulator });
}, initial);
} else if (op === 'all') {
scopedData = jsonLogic.apply(values[0], data);
scopedLogic = values[1];
// All of an empty set is false. Note, some and none have correct fallback after the for loop
if (!Array.isArray(scopedData) || !scopedData.length) {
return false;
}
for (i = 0; i < scopedData.length; i += 1) {
if (!jsonLogic.truthy(jsonLogic.apply(scopedLogic, scopedData[i]))) {
return false; // First falsy, short circuit
}
}
return true; // All were truthy
} else if (op === 'none') {
scopedData = jsonLogic.apply(values[0], data);
scopedLogic = values[1];
if (!Array.isArray(scopedData) || !scopedData.length) {
return true;
}
for (i = 0; i < scopedData.length; i += 1) {
if (jsonLogic.truthy(jsonLogic.apply(scopedLogic, scopedData[i]))) {
return false; // First truthy, short circuit
}
}
return true; // None were truthy
} else if (op === 'some') {
scopedData = jsonLogic.apply(values[0], data);
scopedLogic = values[1];
if (!Array.isArray(scopedData) || !scopedData.length) {
return false;
}
for (i = 0; i < scopedData.length; i += 1) {
if (jsonLogic.truthy(jsonLogic.apply(scopedLogic, scopedData[i]))) {
return true; // First truthy, short circuit
}
}
return false; // None were truthy
}
// Everyone else gets immediate depth-first recursion
values = values.map(function (val) {
return jsonLogic.apply(val, data);
});
// The operation is called with "data" bound to its "this" and "values" passed as arguments.
// Structured commands like % or > can name formal arguments while flexible commands (like missing or merge) can operate on the pseudo-array arguments
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments
if (operations.hasOwnProperty(op) && typeof operations[op] === 'function') {
return operations[op].apply(data, values);
} else if (op.indexOf('.') > 0) {
// Contains a dot, and not in the 0th position
var sub_ops = String(op).split('.');
var operation = operations;
for (i = 0; i < sub_ops.length; i++) {
if (!operation.hasOwnProperty(sub_ops[i])) {
throw new Error('Unrecognized operation ' + op + ' (failed at ' + sub_ops.slice(0, i + 1).join('.') + ')');
}
// Descending into operations
operation = operation[sub_ops[i]];
}
return operation.apply(data, values);
}
throw new Error('Unrecognized operation ' + op);
};
jsonLogic.uses_data = function (logic) {
var collection = [];
if (jsonLogic.is_logic(logic)) {
var op = jsonLogic.get_operator(logic);
var values = logic[op];
if (!Array.isArray(values)) {
values = [values];
}
if (op === 'var') {
// This doesn't cover the case where the arg to var is itself a rule.
collection.push(values[0]);
} else {
// Recursion!
values.forEach(function (val) {
collection.push.apply(collection, jsonLogic.uses_data(val));
});
}
}
return arrayUnique(collection);
};
jsonLogic.add_operation = function (name, code) {
operations[name] = code;
};
jsonLogic.rm_operation = function (name) {
delete operations[name];
};
jsonLogic.rule_like = function (rule, pattern) {
// console.log("Is ". JSON.stringify(rule) . " like " . JSON.stringify(pattern) . "?");
if (pattern === rule) {
return true;
} // TODO : Deep object equivalency?
if (pattern === '@') {
return true;
} // Wildcard!
if (pattern === 'number') {
return typeof rule === 'number';
}
if (pattern === 'string') {
return typeof rule === 'string';
}
if (pattern === 'array') {
// !logic test might be superfluous in JavaScript
return Array.isArray(rule) && !jsonLogic.is_logic(rule);
}
if (jsonLogic.is_logic(pattern)) {
if (jsonLogic.is_logic(rule)) {
var pattern_op = jsonLogic.get_operator(pattern);
var rule_op = jsonLogic.get_operator(rule);
if (pattern_op === '@' || pattern_op === rule_op) {
// echo "\nOperators match, go deeper\n";
return jsonLogic.rule_like(jsonLogic.get_values(rule, false), jsonLogic.get_values(pattern, false));
}
}
return false; // pattern is logic, rule isn't, can't be eq
}
if (Array.isArray(pattern)) {
if (Array.isArray(rule)) {
if (pattern.length !== rule.length) {
return false;
}
/*
Note, array order MATTERS, because we're using this array test logic to consider arguments, where order can matter. (e.g., + is commutative, but '-' or 'if' or 'var' are NOT)
*/
for (var i = 0; i < pattern.length; i += 1) {
// If any fail, we fail
if (!jsonLogic.rule_like(rule[i], pattern[i])) {
return false;
}
}
return true; // If they *all* passed, we pass
} else {
return false; // Pattern is array, rule isn't
}
}
// Not logic, not array, not a === match for rule.
return false;
};
return jsonLogic;
});

View File

@ -0,0 +1,60 @@
import { every, some, get } from 'lodash';
import flat from 'flat';
import jsonLogic from '../../common/utils/logic';
function getDeepestProperty(obj) {
let deepest = {
operator: null,
value: null,
};
let deepestLevel = 0;
function traverse(obj, level) {
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
if (typeof obj[key] === 'object') {
traverse(obj[key], level + 1);
} else {
if (level > deepestLevel) {
deepestLevel = level;
deepest.operator = key;
deepest.value = obj[key];
}
}
}
}
}
traverse(obj, 1);
return deepest;
}
const getValue = (str, values) => {
const regex = /{{(.*?)}}/;
const matches = str?.match?.(regex);
if (matches) {
return getVariableValue(str, values);
} else {
return str;
}
};
const getVariableValue = (str, values) => {
const regex = /{{[^.]+\.([^}]+)}}/;
const match = regex.exec(str);
return values[match?.[1]];
};
export const conditionAnalyse = (rules, values) => {
const type = Object.keys(rules)[0] || '$and';
const conditions = rules[type];
const results = conditions.map((c) => {
const jsonlogic = getDeepestProperty(c);
const operator = jsonlogic.operator;
const value = getValue(jsonlogic.value, values);
const targetField = Object.keys(flat(c))[0]?.replace?.(`.${operator}`, '');
const result = jsonLogic.apply({ [operator]: [flat(values)?.[targetField], value] });
return result;
});
if (type === '$and') {
return every(results, (v) => v);
} else {
return some(results, (v) => v);
}
};

View File

@ -0,0 +1,107 @@
import { css } from '@emotion/css';
import { createForm, onFieldValueChange } from '@formily/core';
import { connect, FieldContext, FormContext } from '@formily/react';
import { merge } from '@formily/shared';
import { Cascader } from 'antd';
import React, { useContext, useMemo } from 'react';
import { SchemaComponent } from '../../schema-component/core';
import { useCompile, useComponent } from '../../schema-component/hooks';
import { FilterContext } from './context';
import { useFilterOptions } from '../../schema-component/antd/filter/useFilterActionProps';
const VariableCascader = connect((props) => {
const fields = useFilterOptions('users');
const compile = useCompile();
const { value, onChange } = props;
return (
<Cascader
className={css`
width: 160px;
`}
value={value ? value.split('.') : []}
fieldNames={{
label: 'title',
value: 'name',
children: 'children',
}}
onChange={(value) => {
onChange(value ? value.join('.') : null);
}}
options={compile([
{
title: '{{t("Current user")}}',
name: 'currentUser',
children: fields
.filter((field) => {
if (!field.target) {
return true;
}
return field.type === 'belongsTo';
})
.map((field) => {
if (field.children) {
field.children = field.children.filter((child) => {
return !child.target;
});
}
return field;
}),
},
])}
/>
);
});
export const DynamicComponent = (props) => {
const { dynamicComponent, disabled } = useContext(FilterContext) || {};
const component = useComponent(dynamicComponent);
const form = useMemo(() => {
return createForm({
values: {
value: props.value,
},
disabled,
effects() {
onFieldValueChange('value', (field) => {
props?.onChange?.(field.value);
});
},
});
}, [JSON.stringify(props.schema), JSON.stringify(props.value)]);
const renderSchemaComponent = () => {
return (
<FieldContext.Provider value={null}>
<SchemaComponent
schema={{
'x-component': 'Input',
...props.schema,
'x-component-props': merge(props?.schema?.['x-component-props'] || {}, {
style: {
minWidth: 150,
width: '100%',
},
}),
name: 'value',
'x-read-pretty': false,
'x-validator': undefined,
'x-decorator': undefined,
}}
components={{
VariableCascader,
}}
/>
</FieldContext.Provider>
);
};
return (
<FormContext.Provider value={form}>
{component
? React.createElement(component, {
value: props.value,
onChange: props?.onChange,
renderSchemaComponent,
})
: renderSchemaComponent()}
</FormContext.Provider>
);
};

View File

@ -0,0 +1,13 @@
import React from 'react';
import { Variable } from '../../schema-component';
import { useVariableOptions } from './Variables';
export function FilterDynamicComponent(props) {
const { value, onChange, renderSchemaComponent, collectionName } = props;
const scope = useVariableOptions(collectionName);
return (
<Variable.Input value={value} onChange={onChange} scope={scope}>
{renderSchemaComponent()}
</Variable.Input>
);
}

View File

@ -0,0 +1,130 @@
import { CloseCircleOutlined } from '@ant-design/icons';
import { css } from '@emotion/css';
import { observer } from '@formily/react';
import { TreeSelect } from '@formily/antd';
import { Select, Space } from 'antd';
import React, { useContext, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useCompile } from '../..';
import { RemoveActionContext } from './context';
import { DynamicComponent } from './DynamicComponent';
import { useValues } from './useValues';
import { ActionType } from './type';
import { ValueDynamicComponent } from './ValueDynamicComponent';
export const FormFieldLinkageRuleAction = observer((props: any) => {
const { value, options, collectionName } = props;
const { t } = useTranslation();
const compile = useCompile();
const remove = useContext(RemoveActionContext);
const {
schema,
fields,
operator,
setDataIndex,
setOperator,
setValue,
value: fieldValue,
operators,
} = useValues(options);
return (
<div style={{ marginBottom: 8 }}>
<Space
className={css`
.ant-space-item {
max-width: 95%;
}
`}
>
<TreeSelect
className={css`
min-width: 160px;
`}
fieldNames={{
label: 'title',
value: 'name',
children: 'children',
}}
treeCheckable={true}
value={value?.targetFields}
multiple
allowClear
treeData={compile(fields)}
onChange={(value) => {
console.log(value)
setDataIndex(value);
}}
placeholder={t('Select Field')}
/>
<Select
value={operator}
className={css`
min-width: 120px;
`}
options={compile(operators)}
onChange={(value) => {
setOperator(value);
}}
placeholder={t('action')}
/>
{[ActionType.Value].includes(operator) && (
<ValueDynamicComponent
fieldValue={fieldValue}
schema={schema}
setValue={setValue}
collectionName={collectionName}
/>
)}
{!props.disabled && (
<a>
<CloseCircleOutlined onClick={() => remove()} style={{ color: '#bfbfbf' }} />
</a>
)}
</Space>
</div>
);
});
export const FormButtonLinkageRuleAction = observer((props: any) => {
const { value, options } = props;
const { t } = useTranslation();
const compile = useCompile();
const [editFalg, setEditFlag] = useState(false);
const remove = useContext(RemoveActionContext);
const { schema, operator, setOperator, setValue } = useValues(options);
const operators = [
{ label: t('Visible'), value: ActionType.Visible, schema: {} },
{ label: t('Hidden'), value: ActionType.Hidden, schema: {} },
{ label: t('Disabled'), value: ActionType.Disabled, schema: {} },
{ label: t('Enabled'), value: ActionType.Active, schema: {} },
];
return (
<div style={{ marginBottom: 8 }}>
<Space>
<Select
value={operator}
options={compile(operators)}
onChange={(value) => {
const flag = [ActionType.Value].includes(value);
setEditFlag(flag);
setOperator(value);
}}
placeholder={t('action')}
/>
{editFalg &&
React.createElement(DynamicComponent, {
value,
schema,
onChange(value) {
setValue(value);
},
})}
{!props.disabled && (
<a>
<CloseCircleOutlined onClick={() => remove()} style={{ color: '#bfbfbf' }} />
</a>
)}
</Space>
</div>
);
});

View File

@ -0,0 +1,52 @@
import { ArrayField as ArrayFieldModel, VoidField } from '@formily/core';
import { ArrayField, ObjectField, observer, useField } from '@formily/react';
import { Space } from 'antd';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { RemoveActionContext } from './context';
import { FormButtonLinkageRuleAction, FormFieldLinkageRuleAction } from './LinkageRuleAction';
export const LinkageRuleActions = observer((props: any): any => {
const { type, linkageOptions } = props;
const field = useField<ArrayFieldModel>();
return field?.value?.map((item, index) => {
return (
<RemoveActionContext.Provider key={index} value={() => field.remove(index)}>
<ObjectField
name={index}
component={[
type === 'button' ? FormButtonLinkageRuleAction : FormFieldLinkageRuleAction,
{ ...props, options: linkageOptions },
]}
/>
</RemoveActionContext.Provider>
);
});
});
export const LinkageRuleActionGroup = (props) => {
const { t } = useTranslation();
const field = useField<VoidField>();
const logic = 'actions';
const { type, linkageOptions, collectionName } = props?.useProps();
return (
<div style={{ marginLeft: 10 }}>
<ArrayField
name={logic}
component={[LinkageRuleActions, { type, linkageOptions, collectionName }]}
disabled={false}
/>
<Space size={16} style={{ marginTop: 8, marginBottom: 8 }}>
<a
onClick={() => {
const f = field.query('.actions').take() as ArrayFieldModel;
const items = f.value || [];
items.push({});
f.value = items;
}}
>
{t('Add property')}
</a>
</Space>
</div>
);
};

View File

@ -0,0 +1,50 @@
import React, { useState } from 'react';
import { Input, Select } from 'antd';
import { useTranslation } from 'react-i18next';
import { DynamicComponent } from './DynamicComponent';
const { Option } = Select;
import { Variable } from '.././../schema-component';
import { useVariableOptions } from './Variables';
export const ValueDynamicComponent = (props) => {
const { fieldValue, schema, setValue, collectionName } = props;
const [mode, setMode] = useState(fieldValue?.mode || 'constant');
const { t } = useTranslation();
const scope = useVariableOptions(collectionName);
return (
<Input.Group compact style={{ minWidth: 280 }}>
<Select value={mode} style={{ width: 150 }} onChange={(value) => setMode(value)}>
<Option value="constant">{t('Constant value')}</Option>
<Option value="express">{t('Expression')}</Option>
</Select>
<div style={{ minWidth: 150, maxWidth: 430 }}>
{mode === 'constant' ? (
React.createElement(DynamicComponent, {
value: fieldValue?.value || fieldValue,
schema,
onChange(value) {
setValue({
mode,
value,
});
},
})
) : (
<Variable.TextArea
value={fieldValue?.value}
onChange={(value) => {
const result = value.replaceAll(`${collectionName}.`, '').replaceAll('$system.', '').trim();
setValue({
mode,
value,
result,
});
}}
scope={scope}
style={{ minWidth: 460, marginRight: 15 }}
/>
)}
</div>
</Input.Group>
);
};

View File

@ -0,0 +1,105 @@
import { useCompile } from '../../schema-component';
import { useCollectionManager, useCollection } from '../../collection-manager';
const supportsType = [
'checkbox',
'number',
'percent',
'integer',
'input',
'textarea',
'email',
'phone',
'datetime',
'createdAt',
'updatedAt',
'radioGroup',
'checkboxGroup',
'select',
'multipleSelect',
'oho',
'obo',
'm2o'
];
const VariableTypes = (currentCollection) => {
const { getCollectionFields, getInterface, getCollection } = useCollectionManager();
const collection = getCollection(currentCollection);
const fields = getCollectionFields(currentCollection);
return [
{
title: collection.title,
value: currentCollection,
options() {
const field2option = (field, depth) => {
if (!field.interface || !supportsType.includes(field.interface)) {
return;
}
const fieldInterface = getInterface(field.interface);
if (!fieldInterface.filterable) {
return;
}
const { nested, children } = fieldInterface.filterable;
const option = {
key: field.name,
label: field?.title || field.uiSchema?.title,
schema: field?.uiSchema,
value: field.name,
};
if (field.target && depth > 2) {
return;
}
if (depth > 2) {
return option;
}
if (children?.length) {
option['children'] = children;
}
if (nested) {
const targetFields = getCollectionFields(field.target);
const options = getOptions(targetFields, depth + 1).filter(Boolean);
option['children'] = option['children'] || [];
option['children'].push(...options);
}
return option;
};
const getOptions = (fields, depth) => {
const options = [];
fields.forEach((field) => {
const option = field2option(field, depth);
if (option) {
options.push(option);
}
});
return options;
};
return getOptions(fields, 1);
},
},
{
title: `{{t("System variables")}}`,
value: '$system',
options: [
{
key: 'now',
value: 'now',
label: `{{t("Current time")}}`,
},
],
},
];
};
export function useVariableOptions(collectionName) {
const compile = useCompile();
const options = VariableTypes(collectionName).map((item: any) => {
const options = typeof item.options === 'function' ? item.options() : item.options;
return {
label: compile(item.title),
value: item.value,
key: item.value,
children: compile(options),
disabled: options && !options.length,
};
});
return options;
}

View File

@ -0,0 +1,61 @@
import { useTranslation } from 'react-i18next';
import { useCollectionManager } from '../../collection-manager';
import { ActionType } from './type';
export const useLinkageCollectionFieldOptions = (collectionName: string) => {
const { getCollectionFields, getInterface } = useCollectionManager();
const fields = getCollectionFields(collectionName).filter((v) => {
return !['id', 'createdAt', 'createdBy', 'updatedAt', 'updatedBy'].includes(v.name);
});
const { t } = useTranslation();
const operators = [
{ label: t('Visible'), value: ActionType.Visible, selected: true, schema: {} },
{ label: t('Editable'), value: ActionType.Editable, selected: false, schema: {} },
{ label: t('Disabled'), value: ActionType.ReadOnly, selected: false, schema: {} },
{ label: t('Easy reading'), value: ActionType.ReadPretty, selected: false, schema: {} },
{ label: t('Hidden'), value: ActionType.None, selected: false, schema: {} },
{ label: t('Hidden(reserved value)'), value: ActionType.Hidden, selected: false, schema: {} },
{ label: t('Required'), value: ActionType.Required, selected: false, schema: {} },
{ label: t('Not required'), value: ActionType.InRequired, selected: false, schema: {} },
{ label: t('Value'), value: ActionType.Value, selected: false, schema: {} },
];
const field2option = (field, depth) => {
const fieldInterface = getInterface(field.interface);
if (!fieldInterface) {
return;
}
const { nested, children } = fieldInterface?.filterable || {};
const option = {
name: field.name,
title: field?.uiSchema?.title || field.name,
schema: field?.uiSchema,
interface: field.interface,
target: field.target,
operators:
operators?.filter?.((operator) => {
if (nested || children || ['formula', 'richText', 'sequence'].includes(fieldInterface.name)) {
return operator?.value !== ActionType.Value;
}
return true;
}) || [],
};
if (field.target && depth > 2) {
return;
}
if (depth > 2) {
return option;
}
return option;
};
const getOptions = (fields, depth) => {
const options = [];
fields.forEach((field) => {
const option = field2option(field, depth);
if (option) {
options.push(option);
}
});
return options;
};
return getOptions(fields, 1);
};

View File

@ -0,0 +1,15 @@
import { ObjectField } from '@formily/core';
import { Schema } from '@formily/react';
import { createContext } from 'react';
export interface FilterContextProps {
field?: ObjectField;
fieldSchema?: Schema;
dynamicComponent?: any;
options?: any[];
disabled?: boolean
}
export const RemoveActionContext = createContext(null);
export const FilterContext = createContext<FilterContextProps>(null);
export const FilterLogicContext = createContext(null);

View File

@ -0,0 +1,117 @@
import { css } from '@emotion/css';
import { observer, useFieldSchema } from '@formily/react';
import React from 'react';
import { SchemaComponent } from '../../schema-component';
import { FilterContext } from '../../schema-component/antd/filter/context';
import { FilterDynamicComponent } from './FilterDynamicComponent';
import { LinkageRuleActionGroup } from './LinkageRuleActionGroup';
export const FormLinkageRules = observer((props: any) => {
const fieldSchema = useFieldSchema();
const { useProps, dynamicComponent } = props;
const { options, defaultValues, collectionName } = useProps();
return (
<FilterContext.Provider value={{ field: options, fieldSchema, dynamicComponent, options: options || [] }}>
<SchemaComponent
schema={{
type: 'object',
properties: {
rules: {
type: 'array',
default: defaultValues,
'x-component': 'ArrayCollapse',
'x-decorator': 'FormItem',
'x-component-props': {
accordion: true,
},
items: {
type: 'object',
'x-component': 'ArrayCollapse.CollapsePanel',
'x-component-props': {
header: '{{ t("Linkage rule") }}',
},
properties: {
index: {
type: 'void',
'x-component': 'ArrayCollapse.Index',
},
layout: {
type: 'void',
'x-component': 'FormLayout',
'x-component-props': {
labelStyle: {
marginTop: '6px',
},
labelCol: 8,
wrapperCol: 16,
},
properties: {
conditions: {
'x-component': 'h4',
'x-content': '{{ t("Condition") }}',
},
condition: {
'x-component': 'Filter',
'x-component-props': {
useProps() {
return {
options,
className: css`
position: relative;
width: 100%;
margin-left: 10px;
`,
};
},
dynamicComponent: (props) => FilterDynamicComponent({ ...props, collectionName }),
},
},
actions: {
'x-component': 'h4',
'x-content': '{{ t("Properties") }}',
},
action: {
type: 'void',
'x-component': LinkageRuleActionGroup,
'x-component-props': {
...props,
},
},
},
},
remove: {
type: 'void',
'x-component': 'ArrayCollapse.Remove',
},
moveUp: {
type: 'void',
'x-component': 'ArrayCollapse.MoveUp',
},
moveDown: {
type: 'void',
'x-component': 'ArrayCollapse.MoveDown',
},
},
},
properties: {
add: {
type: 'void',
title: '{{ t("Add linkage rule") }}',
'x-component': 'ArrayCollapse.Addition',
'x-reactions': {
dependencies: ['rules'],
fulfill: {
state: {
// disabled: '{{$deps[0].length >= 3}}',
},
},
},
},
},
},
},
}}
/>
</FilterContext.Provider>
);
});

View File

@ -0,0 +1,13 @@
export enum ActionType {
Visible = 'visible',
Editable = 'editable',
ReadOnly = 'disabled',
ReadPretty = 'readPretty',
None = 'none',
Hidden = 'hidden',
Required = 'required',
InRequired ='notRequired',
Disabled ='disabled',
Value = 'value',
Active = 'enabled',
}

View File

@ -0,0 +1,59 @@
import { useField } from '@formily/react';
import { useContext, useEffect } from 'react';
import { FilterLogicContext } from './context';
const findOption = (dataIndex = [], options) => {
let items = options;
let option;
dataIndex?.forEach?.((name, index) => {
const item = items.find((v) => v.name === name);
if (item) {
option = item;
}
items = item?.children || [];
});
return option;
};
export const useValues = (options) => {
const field = useField<any>();
const logic = useContext(FilterLogicContext);
const value2data = () => {
field.data = { ...field.initialValue };
const dataIndex = field.initialValue?.targetFields;
const option = (dataIndex && findOption(dataIndex, options)) || {};
const operators = option?.operators || [];
field.data.operators = operators;
field.data.schema = option?.schema;
};
useEffect(value2data, [logic]);
return {
fields: options,
...field.data,
setDataIndex(dataIndex) {
const option = findOption(dataIndex, options);
field.data = field.data || {};
const operators = option?.operators;
field.data.operators = operators?.filter((v) => {
// if (dataIndex.length > 1) {
// return v.value !== 'value';
// }
return true;
});
field.data.schema = option?.schema;
field.value = field.value || [];
field.data.operator = undefined;
field.data.value = undefined;
field.value = { ...field.value, targetFields: dataIndex, operator: undefined, value: undefined };
},
setOperator(operatorValue) {
field.data.operator = operatorValue;
field.value = { ...field.value, operator: operatorValue };
},
setValue(value) {
field.data.value = value;
field.value = { ...field.value, value };
},
};
};

View File

@ -1,7 +1,8 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { FormDialog, FormItem, FormLayout, Input } from '@formily/antd'; import { FormDialog, FormItem, FormLayout, Input, ArrayCollapse } from '@formily/antd';
import { createForm, Field, GeneralField } from '@formily/core'; import { createForm, Field, GeneralField } from '@formily/core';
import { ISchema, Schema, SchemaOptionsContext, useField, useFieldSchema, useForm } from '@formily/react'; import { ISchema, Schema, SchemaOptionsContext, useField, useFieldSchema, useForm } from '@formily/react';
import _ from 'lodash';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { import {
Alert, Alert,
@ -14,7 +15,7 @@ import {
Modal, Modal,
Select, Select,
Space, Space,
Switch Switch,
} from 'antd'; } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
@ -35,10 +36,14 @@ import {
useCollection, useCollection,
useCollectionManager, useCollectionManager,
useCompile, useCompile,
useDesignable useDesignable,
useCollectionFilterOptions,
} from '..'; } from '..';
import { useSchemaTemplateManager } from '../schema-templates'; import { useSchemaTemplateManager } from '../schema-templates';
import { useBlockTemplateContext } from '../schema-templates/BlockTemplate'; import { useBlockTemplateContext } from '../schema-templates/BlockTemplate';
import { FormLinkageRules } from './LinkageRules';
import { useLinkageCollectionFieldOptions } from './LinkageRules/action-hooks';
interface SchemaSettingsProps { interface SchemaSettingsProps {
title?: any; title?: any;
dn?: Designable; dn?: Designable;
@ -223,7 +228,7 @@ const findGridSchema = (fieldSchema) => {
return fieldSchema.reduceProperties((buf, s) => { return fieldSchema.reduceProperties((buf, s) => {
if (s['x-component'] === 'FormV2') { if (s['x-component'] === 'FormV2') {
const f = s.reduceProperties((buf, s) => { const f = s.reduceProperties((buf, s) => {
if (s['x-component'] === 'Grid') { if (s['x-component'] === 'Grid' || s['x-component'] === 'BlockTemplate') {
return s; return s;
} }
return buf; return buf;
@ -629,6 +634,7 @@ SchemaSettings.ModalItem = (props) => {
onSubmit, onSubmit,
asyncGetInitialValues, asyncGetInitialValues,
initialValues, initialValues,
width,
...others ...others
} = props; } = props;
const options = useContext(SchemaOptionsContext); const options = useContext(SchemaOptionsContext);
@ -641,7 +647,7 @@ SchemaSettings.ModalItem = (props) => {
{...others} {...others}
onClick={async () => { onClick={async () => {
const values = asyncGetInitialValues ? await asyncGetInitialValues() : initialValues; const values = asyncGetInitialValues ? await asyncGetInitialValues() : initialValues;
FormDialog(schema.title || title, () => { FormDialog({ title: schema.title || title, width }, () => {
return ( return (
<CollectionManagerContext.Provider value={cm}> <CollectionManagerContext.Provider value={cm}>
<SchemaComponentOptions scope={options.scope} components={options.components}> <SchemaComponentOptions scope={options.scope} components={options.components}>
@ -706,3 +712,61 @@ SchemaSettings.BlockTitleItem = () => {
/> />
); );
}; };
SchemaSettings.LinkageRules = (props) => {
const { collectionName } = props;
const fieldSchema = useFieldSchema();
const { dn } = useDesignable();
const { t } = useTranslation();
const { getTemplateById } = useSchemaTemplateManager();
const type = fieldSchema['x-component'] === 'Action' ? 'button' : 'field';
const gridSchema = findGridSchema(fieldSchema) || fieldSchema;
return (
<SchemaSettings.ModalItem
title={t('Linkage rules')}
components={{ ArrayCollapse, FormLayout }}
width={750}
schema={
{
type: 'object',
title: t('Linkage rules'),
properties: {
fieldReaction: {
'x-component': FormLinkageRules,
'x-component-props': {
useProps: () => {
const options = useCollectionFilterOptions(collectionName);
return {
options,
defaultValues: gridSchema?.['x-linkage-rules'] || fieldSchema?.['x-linkage-rules'],
type,
linkageOptions: useLinkageCollectionFieldOptions(collectionName),
collectionName,
};
},
},
},
},
} as ISchema
}
onSubmit={(v) => {
const rules = [];
for (const rule of v.fieldReaction.rules) {
rules.push(_.pickBy(rule, _.identity));
}
const templateId = gridSchema['x-component'] === 'BlockTemplate' && gridSchema['x-component-props'].templateId;
const uid = (templateId && getTemplateById(templateId).uid) || gridSchema['x-uid'];
const schema = {
['x-uid']: uid,
};
gridSchema['x-linkage-rules'] = rules;
schema['x-linkage-rules'] = rules;
dn.emit('patch', {
schema,
});
dn.refresh();
}}
/>
);
};

View File

@ -18,9 +18,12 @@ export const BlockTemplate = observer((props: any) => {
const { dn } = useDesignable(); const { dn } = useDesignable();
const { t } = useTranslation(); const { t } = useTranslation();
const template = useMemo(() => getTemplateById(templateId), [templateId]); const template = useMemo(() => getTemplateById(templateId), [templateId]);
const onSuccess = (data) => {
fieldSchema['x-linkage-rules'] = data?.data?.['x-linkage-rules'] || [];
};
return template ? ( return template ? (
<BlockTemplateContext.Provider value={{ dn, field, fieldSchema, template }}> <BlockTemplateContext.Provider value={{ dn, field, fieldSchema, template }}>
<RemoteSchemaComponent noForm uid={template?.uid} /> <RemoteSchemaComponent noForm uid={template?.uid} onSuccess={onSuccess} />
</BlockTemplateContext.Provider> </BlockTemplateContext.Provider>
) : null; ) : null;
}); });

View File

@ -24,7 +24,7 @@ const ReadPretty = (props) => {
const Input = (props) => { const Input = (props) => {
const { options } = props; const { options } = props;
const { dataType, expression, engine = 'math.js' } = options; const { dataType, expression, engine = 'math.js' } = options||{};
const { evaluate } = (evaluators as Registry<Evaluator>).get(engine); const { evaluate } = (evaluators as Registry<Evaluator>).get(engine);
const form = useForm(); const form = useForm();
const val = () => { const val = () => {

View File

@ -7,7 +7,7 @@ import Plugin from '..';
import { JOB_STATUS } from '../constants'; import { JOB_STATUS } from '../constants';
export function sleep(ms: number) { export function sleep(ms: number) {
return new Promise((resolve) => { return new Promise(resolve => {
setTimeout(resolve, ms); setTimeout(resolve, ms);
}); });
} }
@ -26,21 +26,21 @@ export async function getApp({ manual, ...options }: MockAppOptions = {}): Promi
run(node, { result }, processor) { run(node, { result }, processor) {
return { return {
status: JOB_STATUS.RESOLVED, status: JOB_STATUS.RESOLVED,
result, result
}; };
}, }
}, },
error: { error: {
run(node, input, processor) { run(node, input, processor) {
throw new Error('definite error'); throw new Error('definite error');
}, }
}, },
'prompt->error': { 'prompt->error': {
run(node, input, processor) { run(node, input, processor) {
return { return {
status: JOB_STATUS.PENDING, status: JOB_STATUS.PENDING
}; };
}, },
resume(node, input, processor) { resume(node, input, processor) {
@ -72,7 +72,7 @@ export async function getApp({ manual, ...options }: MockAppOptions = {}): Promi
await app.load(); await app.load();
await app.db.import({ await app.db.import({
directory: path.resolve(__dirname, './collections'), directory: path.resolve(__dirname, './collections')
}); });
try { try {