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:
parent
8e11e4d561
commit
94e38e453a
@ -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",
|
||||||
|
@ -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": "ファイル形式",
|
||||||
|
@ -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":"分类名称",
|
||||||
|
@ -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')}
|
||||||
|
@ -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();
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -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;
|
||||||
|
@ -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 {
|
||||||
|
@ -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) => {
|
||||||
|
@ -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
|
||||||
|
@ -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} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
109
packages/core/client/src/schema-component/antd/form-v2/utils.tsx
Normal file
109
packages/core/client/src/schema-component/antd/form-v2/utils.tsx
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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';
|
||||||
|
@ -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';
|
||||||
|
@ -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',
|
{
|
||||||
onInput: 'onChange',
|
value: 'checked',
|
||||||
}),
|
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>;
|
||||||
|
@ -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),
|
||||||
);
|
);
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -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 {
|
||||||
|
@ -25,173 +25,176 @@ export const Upload: ComposedUpload = connect(
|
|||||||
mapReadPretty(ReadPretty.Upload),
|
mapReadPretty(ReadPretty.Upload),
|
||||||
);
|
);
|
||||||
|
|
||||||
Upload.Attachment = connect((props: UploadProps) => {
|
Upload.Attachment = connect(
|
||||||
const { disabled, multiple, value, onChange } = props;
|
(props: UploadProps) => {
|
||||||
const [fileList, setFileList] = useState([]);
|
const { disabled, multiple, value, onChange } = props;
|
||||||
const [sync, setSync] = useState(true);
|
const [fileList, setFileList] = useState([]);
|
||||||
const images = fileList;
|
const [sync, setSync] = useState(true);
|
||||||
const [photoIndex, setPhotoIndex] = useState(0);
|
const images = fileList;
|
||||||
const [visible, setVisible] = useState(false);
|
const [photoIndex, setPhotoIndex] = useState(0);
|
||||||
const { t } = useTranslation();
|
const [visible, setVisible] = useState(false);
|
||||||
useEffect(() => {
|
const { t } = useTranslation();
|
||||||
if (sync) {
|
useEffect(() => {
|
||||||
setFileList(toFileList(value));
|
if (sync) {
|
||||||
}
|
setFileList(toFileList(value));
|
||||||
}, [value, sync]);
|
}
|
||||||
const uploadProps = useUploadProps({ ...props });
|
}, [value, sync]);
|
||||||
return (
|
const uploadProps = useUploadProps({ ...props });
|
||||||
<div>
|
return (
|
||||||
<div className={cls('ant-upload-picture-card-wrapper nb-upload')}>
|
<div>
|
||||||
<div className={'ant-upload-list ant-upload-list-picture-card'}>
|
<div className={cls('ant-upload-picture-card-wrapper nb-upload')}>
|
||||||
{fileList.map((file) => {
|
<div className={'ant-upload-list ant-upload-list-picture-card'}>
|
||||||
const handleClick = (e) => {
|
{fileList.map((file) => {
|
||||||
e.preventDefault();
|
const handleClick = (e) => {
|
||||||
e.stopPropagation();
|
e.preventDefault();
|
||||||
const index = fileList.indexOf(file);
|
e.stopPropagation();
|
||||||
if (isImage(file.extname)) {
|
const index = fileList.indexOf(file);
|
||||||
setVisible(true);
|
if (isImage(file.extname)) {
|
||||||
setPhotoIndex(index);
|
setVisible(true);
|
||||||
} else {
|
setPhotoIndex(index);
|
||||||
saveAs(file.url, `${file.title}${file.extname}`);
|
} else {
|
||||||
}
|
saveAs(file.url, `${file.title}${file.extname}`);
|
||||||
};
|
}
|
||||||
return (
|
};
|
||||||
<div className={'ant-upload-list-picture-card-container'}>
|
return (
|
||||||
<div className="ant-upload-list-item ant-upload-list-item-done ant-upload-list-item-list-type-picture-card">
|
<div className={'ant-upload-list-picture-card-container'}>
|
||||||
<div className={'ant-upload-list-item-info'}>
|
<div className="ant-upload-list-item ant-upload-list-item-done ant-upload-list-item-list-type-picture-card">
|
||||||
<span className="ant-upload-span">
|
<div className={'ant-upload-list-item-info'}>
|
||||||
<a
|
<span className="ant-upload-span">
|
||||||
className="ant-upload-list-item-thumbnail"
|
<a
|
||||||
href={file.url}
|
className="ant-upload-list-item-thumbnail"
|
||||||
target="_blank"
|
href={file.url}
|
||||||
rel="noopener noreferrer"
|
target="_blank"
|
||||||
onClick={handleClick}
|
rel="noopener noreferrer"
|
||||||
>
|
onClick={handleClick}
|
||||||
{file.imageUrl && (
|
>
|
||||||
<img src={file.imageUrl} alt={file.title} className="ant-upload-list-item-image" />
|
{file.imageUrl && (
|
||||||
)}
|
<img src={file.imageUrl} alt={file.title} className="ant-upload-list-item-image" />
|
||||||
</a>
|
)}
|
||||||
<a
|
</a>
|
||||||
target="_blank"
|
<a
|
||||||
rel="noopener noreferrer"
|
target="_blank"
|
||||||
className="ant-upload-list-item-name"
|
rel="noopener noreferrer"
|
||||||
title={file.title}
|
className="ant-upload-list-item-name"
|
||||||
href={file.url}
|
title={file.title}
|
||||||
onClick={handleClick}
|
href={file.url}
|
||||||
>
|
onClick={handleClick}
|
||||||
{file.status === 'uploading' ? t('Uploading') : file.title}
|
>
|
||||||
</a>
|
{file.status === 'uploading' ? t('Uploading') : file.title}
|
||||||
</span>
|
</a>
|
||||||
</div>
|
</span>
|
||||||
<span className={'ant-upload-list-item-actions'}>
|
</div>
|
||||||
<Space size={3}>
|
<span className={'ant-upload-list-item-actions'}>
|
||||||
<Button
|
<Space size={3}>
|
||||||
size={'small'}
|
|
||||||
type={'text'}
|
|
||||||
icon={<DownloadOutlined />}
|
|
||||||
onClick={() => {
|
|
||||||
saveAs(file.url, `${file.title}${file.extname}`);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{!disabled && (
|
|
||||||
<Button
|
<Button
|
||||||
size={'small'}
|
size={'small'}
|
||||||
type={'text'}
|
type={'text'}
|
||||||
icon={<DeleteOutlined />}
|
icon={<DownloadOutlined />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSync(false);
|
saveAs(file.url, `${file.title}${file.extname}`);
|
||||||
setFileList((prevFileList) => {
|
|
||||||
if (!multiple) {
|
|
||||||
onChange(null);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const index = prevFileList.indexOf(file);
|
|
||||||
prevFileList.splice(index, 1);
|
|
||||||
onChange(toValue([...prevFileList]));
|
|
||||||
return [...prevFileList];
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
{!disabled && (
|
||||||
</Space>
|
<Button
|
||||||
</span>
|
size={'small'}
|
||||||
{file.status === 'uploading' && (
|
type={'text'}
|
||||||
<div className={'ant-upload-list-item-progress'}>
|
icon={<DeleteOutlined />}
|
||||||
<Progress strokeWidth={2} type={'line'} showInfo={false} percent={file.percent} />
|
onClick={() => {
|
||||||
</div>
|
setSync(false);
|
||||||
)}
|
setFileList((prevFileList) => {
|
||||||
|
if (!multiple) {
|
||||||
|
onChange(null);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const index = prevFileList.indexOf(file);
|
||||||
|
prevFileList.splice(index, 1);
|
||||||
|
onChange(toValue([...prevFileList]));
|
||||||
|
return [...prevFileList];
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</span>
|
||||||
|
{file.status === 'uploading' && (
|
||||||
|
<div className={'ant-upload-list-item-progress'}>
|
||||||
|
<Progress strokeWidth={2} type={'line'} showInfo={false} percent={file.percent} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{!disabled && (multiple || toArr(value).length < 1) && (
|
||||||
|
<div className={'ant-upload-list-picture-card-container'}>
|
||||||
|
<AntdUpload
|
||||||
|
{...uploadProps}
|
||||||
|
disabled={disabled}
|
||||||
|
multiple={multiple}
|
||||||
|
listType={'picture-card'}
|
||||||
|
fileList={fileList}
|
||||||
|
onChange={(info) => {
|
||||||
|
setSync(false);
|
||||||
|
if (multiple) {
|
||||||
|
if (info.file.status === 'done') {
|
||||||
|
onChange(toValue(info.fileList));
|
||||||
|
}
|
||||||
|
setFileList(info.fileList.map(toItem));
|
||||||
|
} else {
|
||||||
|
if (info.file.status === 'done') {
|
||||||
|
// TODO(BUG): object 的联动有问题,不响应,折中的办法先置空再赋值
|
||||||
|
onChange(null);
|
||||||
|
onChange(info.file?.response?.data);
|
||||||
|
}
|
||||||
|
setFileList([toItem(info.file)]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
showUploadList={false}
|
||||||
|
>
|
||||||
|
{!disabled && (multiple || toArr(value).length < 1) && (
|
||||||
|
<span>
|
||||||
|
<PlusOutlined />
|
||||||
|
<br /> {t('Upload')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</AntdUpload>
|
||||||
</div>
|
</div>
|
||||||
);
|
)}
|
||||||
})}
|
</div>
|
||||||
{!disabled && (multiple || toArr(value).length < 1) && (
|
|
||||||
<div className={'ant-upload-list-picture-card-container'}>
|
|
||||||
<AntdUpload
|
|
||||||
{...uploadProps}
|
|
||||||
disabled={disabled}
|
|
||||||
multiple={multiple}
|
|
||||||
listType={'picture-card'}
|
|
||||||
fileList={fileList}
|
|
||||||
onChange={(info) => {
|
|
||||||
setSync(false);
|
|
||||||
if (multiple) {
|
|
||||||
if (info.file.status === 'done') {
|
|
||||||
onChange(toValue(info.fileList));
|
|
||||||
}
|
|
||||||
setFileList(info.fileList.map(toItem));
|
|
||||||
} else {
|
|
||||||
if (info.file.status === 'done') {
|
|
||||||
// TODO(BUG): object 的联动有问题,不响应,折中的办法先置空再赋值
|
|
||||||
onChange(null);
|
|
||||||
onChange(info.file?.response?.data);
|
|
||||||
}
|
|
||||||
setFileList([toItem(info.file)]);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
showUploadList={false}
|
|
||||||
>
|
|
||||||
{!disabled && (multiple || toArr(value).length < 1) && (
|
|
||||||
<span>
|
|
||||||
<PlusOutlined />
|
|
||||||
<br /> {t('Upload')}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</AntdUpload>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
{visible && (
|
||||||
|
<Lightbox
|
||||||
|
// discourageDownloads={true}
|
||||||
|
mainSrc={images[photoIndex]?.imageUrl}
|
||||||
|
nextSrc={images[(photoIndex + 1) % images.length]?.imageUrl}
|
||||||
|
prevSrc={images[(photoIndex + images.length - 1) % images.length]?.imageUrl}
|
||||||
|
onCloseRequest={() => setVisible(false)}
|
||||||
|
onMovePrevRequest={() => setPhotoIndex((photoIndex + images.length - 1) % images.length)}
|
||||||
|
onMoveNextRequest={() => setPhotoIndex((photoIndex + 1) % images.length)}
|
||||||
|
imageTitle={images[photoIndex]?.title}
|
||||||
|
toolbarButtons={[
|
||||||
|
<button
|
||||||
|
style={{ fontSize: 22, background: 'none', lineHeight: 1 }}
|
||||||
|
type="button"
|
||||||
|
aria-label="Zoom in"
|
||||||
|
title="Zoom in"
|
||||||
|
className="ril-zoom-in ril__toolbarItemChild ril__builtinButton"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const file = images[photoIndex];
|
||||||
|
saveAs(file.url, `${file.title}${file.extname}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DownloadOutlined />
|
||||||
|
</button>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{visible && (
|
);
|
||||||
<Lightbox
|
},
|
||||||
// discourageDownloads={true}
|
mapReadPretty(ReadPretty.Attachment),
|
||||||
mainSrc={images[photoIndex]?.imageUrl}
|
);
|
||||||
nextSrc={images[(photoIndex + 1) % images.length]?.imageUrl}
|
|
||||||
prevSrc={images[(photoIndex + images.length - 1) % images.length]?.imageUrl}
|
|
||||||
onCloseRequest={() => setVisible(false)}
|
|
||||||
onMovePrevRequest={() => setPhotoIndex((photoIndex + images.length - 1) % images.length)}
|
|
||||||
onMoveNextRequest={() => setPhotoIndex((photoIndex + 1) % images.length)}
|
|
||||||
imageTitle={images[photoIndex]?.title}
|
|
||||||
toolbarButtons={[
|
|
||||||
<button
|
|
||||||
style={{ fontSize: 22, background: 'none', lineHeight: 1 }}
|
|
||||||
type="button"
|
|
||||||
aria-label="Zoom in"
|
|
||||||
title="Zoom in"
|
|
||||||
className="ril-zoom-in ril__toolbarItemChild ril__builtinButton"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const file = images[photoIndex];
|
|
||||||
saveAs(file.url, `${file.title}${file.extname}`);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DownloadOutlined />
|
|
||||||
</button>,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}, mapReadPretty(ReadPretty.Attachment));
|
|
||||||
|
|
||||||
Upload.Dragger = connect(
|
Upload.Dragger = connect(
|
||||||
(props: DraggerProps) => {
|
(props: DraggerProps) => {
|
||||||
|
@ -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,46 +134,52 @@ 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) => {
|
||||||
return option ? opts.concat(option) : opts;
|
const option = (i ? (opts[i - 1] as VariableOptions).children : options)?.find((item) => item.value === key);
|
||||||
}, [] as VariableOptions[]).map(item => item.label).join(' / ');
|
return option ? opts.concat(option) : opts;
|
||||||
|
}, [] 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
|
||||||
width: auto;
|
compact
|
||||||
.ant-input-disabled{
|
className={css`
|
||||||
.ant-tag{
|
width: auto;
|
||||||
color: #bfbfbf;
|
display: flex !important;
|
||||||
border-color: #d9d9d9;
|
.ant-input-disabled {
|
||||||
|
.ant-tag {
|
||||||
|
color: #bfbfbf;
|
||||||
|
border-color: #d9d9d9;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
.ant-input.null-value {
|
||||||
.ant-input.null-value{
|
width: 4em;
|
||||||
width: 4em;
|
min-width: 4em;
|
||||||
min-width: 4em;
|
}
|
||||||
}
|
`}
|
||||||
`}
|
|
||||||
>
|
>
|
||||||
{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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-input{
|
.ant-input {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
${disabled ? '' : 'padding-right: 28px;'}
|
${disabled ? '' : 'padding-right: 28px;'}
|
||||||
|
|
||||||
.ant-tag{
|
.ant-tag {
|
||||||
display: inline;
|
display: inline;
|
||||||
line-height: 19px;
|
line-height: 19px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -195,64 +187,59 @@ export function Input(props) {
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`}>
|
`}
|
||||||
<div
|
>
|
||||||
onInput={e => e.preventDefault()}
|
<div
|
||||||
onKeyDown={(e) => {
|
onInput={(e) => e.preventDefault()}
|
||||||
if (e.key !== 'Backspace') {
|
onKeyDown={(e) => {
|
||||||
e.preventDefault();
|
if (e.key !== 'Backspace') {
|
||||||
return;
|
e.preventDefault();
|
||||||
}
|
return;
|
||||||
onChange(null);
|
}
|
||||||
}}
|
onChange(null);
|
||||||
className={cx('ant-input', { 'ant-input-disabled': disabled })}
|
}}
|
||||||
contentEditable={!disabled}
|
className={cx('ant-input', { 'ant-input-disabled': disabled })}
|
||||||
suppressContentEditableWarning
|
contentEditable={!disabled}
|
||||||
>
|
suppressContentEditableWarning
|
||||||
<Tag contentEditable={false} color="blue">{variableText}</Tag>
|
|
||||||
</div>
|
|
||||||
{!disabled
|
|
||||||
? (
|
|
||||||
<span
|
|
||||||
className={cx('ant-select-clear', css`
|
|
||||||
user-select: 'none'
|
|
||||||
`)}
|
|
||||||
unselectable="on"
|
|
||||||
aria-hidden
|
|
||||||
onClick={() => onChange(null)}
|
|
||||||
>
|
|
||||||
<CloseCircleFilled />
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
: null}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
: (
|
|
||||||
children ?? <ConstantComponent value={value} onChange={onChange} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{options.length > 1
|
|
||||||
? (
|
|
||||||
<Cascader
|
|
||||||
value={variable ?? ['', ...(children ? [] : [type])]}
|
|
||||||
options={options}
|
|
||||||
onChange={onSwitch}
|
|
||||||
>
|
>
|
||||||
{button ?? (
|
<Tag contentEditable={false} color="blue">
|
||||||
<Button
|
{variableText}
|
||||||
type={variable ? 'primary' : 'default'}
|
</Tag>
|
||||||
className={css`
|
</div>
|
||||||
font-style: italic;
|
{!disabled ? (
|
||||||
font-family: "New York", "Times New Roman", Times, serif;
|
<span
|
||||||
`}
|
className={cx(
|
||||||
>
|
'ant-select-clear',
|
||||||
x
|
css`
|
||||||
</Button>
|
user-select: 'none';
|
||||||
)}
|
`,
|
||||||
</Cascader>
|
)}
|
||||||
)
|
unselectable="on"
|
||||||
: null
|
aria-hidden
|
||||||
}
|
onClick={() => onChange(null)}
|
||||||
|
>
|
||||||
|
<CloseCircleFilled />
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
children ?? <ConstantComponent value={value} onChange={onChange} />
|
||||||
|
)}
|
||||||
|
{options.length > 1 ? (
|
||||||
|
<Cascader value={variable ?? ['', ...(children ? [] : [type])]} options={options} onChange={onSwitch}>
|
||||||
|
{button ?? (
|
||||||
|
<Button
|
||||||
|
type={variable ? 'primary' : 'default'}
|
||||||
|
className={css`
|
||||||
|
font-style: italic;
|
||||||
|
font-family: 'New York', 'Times New Roman', Times, serif;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
x
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Cascader>
|
||||||
|
) : null}
|
||||||
</AntInput.Group>
|
</AntInput.Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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,20 +176,24 @@ 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
|
||||||
&.ant-input-group.ant-input-group-compact{
|
compact
|
||||||
display: flex;
|
className={css`
|
||||||
.ant-input{
|
&.ant-input-group.ant-input-group-compact {
|
||||||
flex-grow: 1;
|
display: flex;
|
||||||
}
|
.ant-input {
|
||||||
.ant-input-disabled{
|
flex-grow: 1;
|
||||||
.ant-tag{
|
min-width: 200px;
|
||||||
color: #bfbfbf;
|
}
|
||||||
border-color: #d9d9d9;
|
.ant-input-disabled {
|
||||||
|
.ant-tag {
|
||||||
|
color: #bfbfbf;
|
||||||
|
border-color: #d9d9d9;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
`}
|
||||||
`}>
|
>
|
||||||
<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(
|
||||||
overflow: auto;
|
'ant-input',
|
||||||
white-space: ${multiline ? 'normal': 'nowrap'};
|
{ 'ant-input-disabled': disabled },
|
||||||
|
css`
|
||||||
|
overflow: auto;
|
||||||
|
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
|
||||||
|
487
packages/core/client/src/schema-component/common/utils/logic.js
Normal file
487
packages/core/client/src/schema-component/common/utils/logic.js
Normal 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;
|
||||||
|
});
|
@ -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);
|
||||||
|
}
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
@ -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>
|
||||||
|
);
|
||||||
|
});
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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;
|
||||||
|
}
|
@ -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);
|
||||||
|
};
|
@ -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);
|
117
packages/core/client/src/schema-settings/LinkageRules/index.tsx
Normal file
117
packages/core/client/src/schema-settings/LinkageRules/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
});
|
@ -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',
|
||||||
|
}
|
@ -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 };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
@ -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();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -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;
|
||||||
});
|
});
|
||||||
|
@ -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 = () => {
|
||||||
|
@ -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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user