fix(linkage rules) : support naming, enabling and disabling, copying, and assigning null values (#1511)
* fix: condiction suport empty * fix: condiction suport empty * fix: condiction suport empty * fix: condiction suport empty * feat: detailed operations support linkage rules * fix: button linkage failed during page change of detail block * fix: button linkage failed during page change of detail block * fix: linkage values not suport formula * fix: linkage value suport empty * fix: checkbox value linkage failed * refactor: using evaluators package * refactor: using evaluators package * refactor: linkage field * fix: relation field is limited to one level * fix: relation field is limited to one level * fix: linkage local * feat: support copy linkage rules * feat: support naming linkage rules * feat: support naming linkage rules * style: style improve * fix: missing operation when copying linkage * feat: linkage rules support disabling and enabling * fix: linkage rule update is not updated immediately * feat: action support linkage enabled * feat: action support linkage enabled * feat: action support linkage enabled * fix: linkage rules title update exception * feat: action support linkage enabled * fix: linkage rules copy failed * fix: linkage rules copy failed * fix: linkage rules copy failed * fix: linkage rules copy failed * refactor: code improve * refactor: code improve * fix: linkage rules copy failed * fix: linkage rules copy failed * fix: the first input is cleared * refactor: code improve * refactor: code improve * refactor: delete redundant variables * fix: action modal automatically closes * fix: action automatically closes * fix: action automatically closes * fix: details block chang page action linkage failed * fix: details block chang page action linkage failed * feat: syntax references * feat: syntax references * fix: current time * fix: current time * fix: action linkage * fix: formulajs tyy catch * fix: multi-level linkage * chore: linkage support * chore: linkage support * chore: linkage code improve --------- Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
parent
02758cb338
commit
c2885ee1d6
@ -126,6 +126,7 @@ export const useCollectionFilterOptions = (collectionName: string) => {
|
||||
operators?.filter?.((operator) => {
|
||||
return !operator?.visible || operator.visible(field);
|
||||
}) || [],
|
||||
interface: field.interface,
|
||||
};
|
||||
if (field.target && depth > 2) {
|
||||
return;
|
||||
|
@ -52,6 +52,7 @@ export default {
|
||||
"Value":"Value",
|
||||
"Disabled":"Disabled",
|
||||
"Enabled":"Enabled",
|
||||
"Empty":"Empty",
|
||||
"Linkage rule":"Linkage rule",
|
||||
"Linkage rules":"Linkage rules",
|
||||
"Condition":"Condition",
|
||||
|
@ -58,6 +58,7 @@ export default {
|
||||
"Value":"フィールド値",
|
||||
"Disabled":"無効化",
|
||||
"Enabled":"有効化",
|
||||
"Empty":"くうきち",
|
||||
"Linkage rule":"連動規則",
|
||||
"Linkage rules":"連動規則",
|
||||
"Condition":"条件#ジョウケン#",
|
||||
|
@ -50,6 +50,7 @@ export default {
|
||||
"Value":"字段值",
|
||||
"Disabled":"禁用",
|
||||
"Enabled":"启用",
|
||||
"Empty":"赋空值",
|
||||
"Linkage rule":"联动规则",
|
||||
"Linkage rules":"联动规则",
|
||||
"Condition":"条件",
|
||||
|
@ -2,13 +2,14 @@ import { css } from '@emotion/css';
|
||||
import { observer, RecursionField, useField, useFieldSchema, useForm } from '@formily/react';
|
||||
import { Button, Modal, Popover } from 'antd';
|
||||
import classnames from 'classnames';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useActionContext } from '../..';
|
||||
import { useDesignable } from '../../';
|
||||
import { Icon } from '../../../icon';
|
||||
import { useRecord } from '../../../record-provider';
|
||||
import { SortableItem } from '../../common';
|
||||
import { useCompile, useComponent, useDesigner } from '../../hooks';
|
||||
import { useProps } from '../../hooks/useProps';
|
||||
import { useRecord } from '../../../record-provider';
|
||||
import ActionContainer from './Action.Container';
|
||||
import { ActionDesigner } from './Action.Designer';
|
||||
import { ActionDrawer } from './Action.Drawer';
|
||||
@ -18,7 +19,6 @@ import { ActionPage } from './Action.Page';
|
||||
import { ActionContext } from './context';
|
||||
import { useA } from './hooks';
|
||||
import { ComposedAction } from './types';
|
||||
import { useDesignable } from '../../';
|
||||
import { linkageAction } from './utils';
|
||||
|
||||
export const actionDesignerCss = css`
|
||||
@ -96,15 +96,17 @@ export const Action: ComposedAction = observer((props: any) => {
|
||||
const disabled = form.disabled || field.disabled;
|
||||
const openSize = fieldSchema?.['x-component-props']?.['openSize'];
|
||||
const linkageRules = fieldSchema?.['x-linkage-rules'] || [];
|
||||
const { designable, } = useDesignable();
|
||||
const tarComponent=useComponent(component)||component;
|
||||
const { designable } = useDesignable();
|
||||
const tarComponent = useComponent(component) || component;
|
||||
useEffect(() => {
|
||||
field.linkageProperty = {};
|
||||
linkageRules.map((v) => {
|
||||
return v.actions?.map((h) => {
|
||||
linkageAction(h.operator, field, v.condition, values);
|
||||
linkageRules
|
||||
.filter((k) => !k.disabled)
|
||||
.map((v) => {
|
||||
return v.actions?.map((h) => {
|
||||
linkageAction(h.operator, field, v.condition, values);
|
||||
});
|
||||
});
|
||||
});
|
||||
}, [linkageRules, values]);
|
||||
const renderButton = () => (
|
||||
<SortableItem
|
||||
|
@ -69,7 +69,8 @@ const WithForm = (props) => {
|
||||
const { form } = props;
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { setFormValueChanged } = useActionContext();
|
||||
const linkageRules = getLinkageRules(fieldSchema) || fieldSchema.parent?.['x-linkage-rules'] || [];
|
||||
const linkageRules =
|
||||
(getLinkageRules(fieldSchema) || fieldSchema.parent?.['x-linkage-rules'])?.filter((k) => !k.disabled) || [];
|
||||
useEffect(() => {
|
||||
const id = uid();
|
||||
form.addEffects(id, () => {
|
||||
@ -100,7 +101,9 @@ const WithForm = (props) => {
|
||||
};
|
||||
});
|
||||
onFieldChange(`*(${fields})`, ['value', 'required', 'pattern', 'display'], (field: any) => {
|
||||
field.linkageProperty = {};
|
||||
field.linkageProperty = {
|
||||
display: field.linkageProperty?.display,
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -1,47 +1,13 @@
|
||||
import { last, get } from 'lodash';
|
||||
import * as functions from '@formulajs/formulajs';
|
||||
import { last, cloneDeep } from 'lodash';
|
||||
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);
|
||||
}
|
||||
import evaluators from '@nocobase/evaluators/client';
|
||||
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];
|
||||
const { evaluate } = evaluators.get('formula.js');
|
||||
|
||||
switch (operator) {
|
||||
case ActionType.Required:
|
||||
@ -91,17 +57,23 @@ export const linkageMergeAction = ({ operator, value }, field, condition, values
|
||||
case ActionType.Value:
|
||||
if (conditionAnalyse(condition, values)) {
|
||||
if (value?.mode === 'express') {
|
||||
const result = evaluate(value.result || value.value, values);
|
||||
valueResult.push(result);
|
||||
} else {
|
||||
const scope = cloneDeep(values);
|
||||
try {
|
||||
const result = evaluate(value.result || value.value, { ...scope, now: () => new Date().toString() });
|
||||
valueResult.push(result);
|
||||
} catch (error) {
|
||||
}
|
||||
} else if (value?.mode === 'constant') {
|
||||
valueResult.push(value?.value || value);
|
||||
} else {
|
||||
valueResult.push(null);
|
||||
}
|
||||
}
|
||||
field.linkageProperty = {
|
||||
...field.linkageProperty,
|
||||
value: valueResult,
|
||||
};
|
||||
setTimeout(() => (field.value = last(valueResult) === undefined ? field.value : last(valueResult)));
|
||||
field.value = last(valueResult) === undefined ? field.value : last(valueResult);
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
|
@ -39,7 +39,10 @@ http://ricostacruz.com/cheatsheets/umdjs.html
|
||||
var jsonLogic = {};
|
||||
var operations = {
|
||||
$is: function (a, b) {
|
||||
return a == b;
|
||||
return a === b;
|
||||
},
|
||||
$match: function (a, b) {
|
||||
return JSON.stringify(a) === JSON.stringify(b);
|
||||
},
|
||||
$eq: function (a, b) {
|
||||
return a === b;
|
||||
@ -87,10 +90,16 @@ http://ricostacruz.com/cheatsheets/umdjs.html
|
||||
if (!a || typeof a.indexOf === 'undefined') return false;
|
||||
return a.indexOf(b) !== -1;
|
||||
},
|
||||
$notIncludes:function (a, b) {
|
||||
$notIncludes: function (a, b) {
|
||||
if (!a || typeof a.indexOf === 'undefined') return false;
|
||||
return !(a.indexOf(b) !== -1);
|
||||
},
|
||||
$isTruly: function (a) {
|
||||
return a === true || a === 1;
|
||||
},
|
||||
$isFalsy: function (a) {
|
||||
return !jsonLogic.truthy(a);
|
||||
},
|
||||
cat: function () {
|
||||
return Array.prototype.join.call(arguments, '');
|
||||
},
|
||||
|
@ -1,30 +1,21 @@
|
||||
import { every, some, get } from 'lodash';
|
||||
import { every, some, findIndex } 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];
|
||||
}
|
||||
}
|
||||
function getInnermostKeyAndValue(obj) {
|
||||
if (typeof obj !== 'object' || obj === null) {
|
||||
return null;
|
||||
}
|
||||
for (const key in obj) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
if (Object.prototype.toString.call(obj[key]) === '[object Object]' && obj[key] !== null) {
|
||||
return getInnermostKeyAndValue(obj[key]);
|
||||
} else {
|
||||
return { key, value: obj[key] };
|
||||
}
|
||||
}
|
||||
}
|
||||
traverse(obj, 1);
|
||||
return deepest;
|
||||
return null;
|
||||
}
|
||||
const getValue = (str, values) => {
|
||||
const regex = /{{(.*?)}}/;
|
||||
@ -40,20 +31,47 @@ const getVariableValue = (str, values) => {
|
||||
const match = regex.exec(str);
|
||||
return values[match?.[1]];
|
||||
};
|
||||
const getTargetField = (obj) => {
|
||||
const keys = getAllKeys(obj);
|
||||
const index = findIndex(keys, (key, index, keys) => {
|
||||
if (key.includes('$') && index > 0) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
const result = keys.slice(0, index);
|
||||
return result;
|
||||
};
|
||||
|
||||
function getAllKeys(obj) {
|
||||
const keys = [];
|
||||
function traverse(o) {
|
||||
Object.keys(o)
|
||||
.sort()
|
||||
.forEach(function (key) {
|
||||
keys.push(key);
|
||||
if (o[key] && typeof o[key] === 'object') {
|
||||
traverse(o[key]);
|
||||
}
|
||||
});
|
||||
}
|
||||
traverse(obj);
|
||||
return keys;
|
||||
}
|
||||
|
||||
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 jsonlogic = getInnermostKeyAndValue(c);
|
||||
const operator = jsonlogic?.key;
|
||||
const value = getValue(jsonlogic?.value, values);
|
||||
const targetField = getTargetField(c);
|
||||
if (!operator) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
const result = jsonLogic.apply({ [operator]: [flat(values)?.[targetField], value] });
|
||||
const currentValue = targetField.length > 1 ? flat(values)?.[targetField.join('.')] : values?.[targetField[0]];
|
||||
const result = jsonLogic.apply({ [operator]: [currentValue, value] });
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { CloseCircleOutlined } from '@ant-design/icons';
|
||||
import { css } from '@emotion/css';
|
||||
import { observer } from '@formily/react';
|
||||
import { observer, useField } from '@formily/react';
|
||||
import { TreeSelect } from '@formily/antd';
|
||||
import { Select, Space } from 'antd';
|
||||
import { Select, Space } from 'antd';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { uid } from '@formily/shared';
|
||||
import { useCompile } from '../..';
|
||||
import { RemoveActionContext } from './context';
|
||||
import { RemoveActionContext, LinkageLogicContext } from './context';
|
||||
import { DynamicComponent } from './DynamicComponent';
|
||||
import { useValues } from './useValues';
|
||||
import { ActionType } from './type';
|
||||
@ -28,60 +29,61 @@ export const FormFieldLinkageRuleAction = observer((props: any) => {
|
||||
operators,
|
||||
} = useValues(options);
|
||||
return (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<Space
|
||||
className={css`
|
||||
.ant-space-item {
|
||||
max-width: 95%;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<TreeSelect
|
||||
<LinkageLogicContext.Provider value={uid()}>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<Space
|
||||
className={css`
|
||||
min-width: 160px;
|
||||
.ant-space-item {
|
||||
max-width: 95%;
|
||||
}
|
||||
`}
|
||||
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}
|
||||
>
|
||||
<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) => {
|
||||
setDataIndex(value);
|
||||
}}
|
||||
placeholder={t('Select Field')}
|
||||
/>
|
||||
)}
|
||||
{!props.disabled && (
|
||||
<a>
|
||||
<CloseCircleOutlined onClick={() => remove()} style={{ color: '#bfbfbf' }} />
|
||||
</a>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
<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>
|
||||
</LinkageLogicContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -1,25 +1,37 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Input, Select } from 'antd';
|
||||
import { css } from '@emotion/css';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DynamicComponent } from './DynamicComponent';
|
||||
const { Option } = Select;
|
||||
import { Variable } from '.././../schema-component';
|
||||
import { useVariableOptions } from './Variables';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
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)}>
|
||||
<Input.Group compact>
|
||||
<Select
|
||||
value={mode}
|
||||
style={{ width: 150 }}
|
||||
onChange={(value) => {
|
||||
setMode(value);
|
||||
setValue({
|
||||
mode: value,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Option value="constant">{t('Constant value')}</Option>
|
||||
<Option value="express">{t('Expression')}</Option>
|
||||
<Option value="empty">{t('Empty')}</Option>
|
||||
</Select>
|
||||
<div style={{ minWidth: 150, maxWidth: 430 }}>
|
||||
{mode === 'constant' ? (
|
||||
React.createElement(DynamicComponent, {
|
||||
{mode === 'constant' ? (
|
||||
<div style={{ minWidth: 150, maxWidth: 430 }}>
|
||||
{React.createElement(DynamicComponent, {
|
||||
value: fieldValue?.value || fieldValue,
|
||||
schema,
|
||||
onChange(value) {
|
||||
@ -28,8 +40,10 @@ export const ValueDynamicComponent = (props) => {
|
||||
value,
|
||||
});
|
||||
},
|
||||
})
|
||||
) : (
|
||||
})}
|
||||
</div>
|
||||
) : mode === 'express' ? (
|
||||
<div style={{ minWidth: 150, maxWidth: 430, fontSize: 13 }}>
|
||||
<Variable.TextArea
|
||||
value={fieldValue?.value}
|
||||
onChange={(value) => {
|
||||
@ -43,8 +57,16 @@ export const ValueDynamicComponent = (props) => {
|
||||
scope={scope}
|
||||
style={{ minWidth: 460, marginRight: 15 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<>
|
||||
<span style={{ marginLeft: '.25em' }} className={'ant-formily-item-extra'}>
|
||||
{t('Syntax references')}:
|
||||
</span>
|
||||
<a href="https://formulajs.info/functions/" target="_blank">
|
||||
Formula.js
|
||||
</a>
|
||||
</>
|
||||
</div>
|
||||
) : null}
|
||||
</Input.Group>
|
||||
);
|
||||
};
|
||||
|
@ -17,9 +17,12 @@ const supportsType = [
|
||||
'checkboxGroup',
|
||||
'select',
|
||||
'multipleSelect',
|
||||
'formula',
|
||||
'oho',
|
||||
'obo',
|
||||
'm2o',
|
||||
'o2m',
|
||||
'm2m',
|
||||
];
|
||||
const useVariableTypes = (currentCollection) => {
|
||||
const { getCollectionFields, getInterface, getCollection } = useCollectionManager();
|
||||
@ -45,10 +48,10 @@ const useVariableTypes = (currentCollection) => {
|
||||
schema: field?.uiSchema,
|
||||
value: field.name,
|
||||
};
|
||||
if (field.target && depth > 2) {
|
||||
if (field.target && depth > 1) {
|
||||
return;
|
||||
}
|
||||
if (depth > 2) {
|
||||
if (depth > 1) {
|
||||
return option;
|
||||
}
|
||||
if (children?.length) {
|
||||
|
@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { Switch } from 'antd';
|
||||
import { ArrayBase } from '@formily/antd';
|
||||
|
||||
export const EnableLinkage = React.forwardRef((props: any, ref) => {
|
||||
const array = ArrayBase.useArray();
|
||||
const index = ArrayBase.useIndex(props.index);
|
||||
return (
|
||||
<Switch
|
||||
{...props}
|
||||
checked={!array?.field?.value[index].disabled}
|
||||
size={'small'}
|
||||
style={{
|
||||
transition: 'all 0.25s ease-in-out',
|
||||
color: 'rgba(0, 0, 0, 0.8)',
|
||||
fontSize: 16,
|
||||
marginLeft: 6,
|
||||
marginBottom: 3,
|
||||
}}
|
||||
onChange={(checked, e) => {
|
||||
e.stopPropagation();
|
||||
array.field.value.splice(index, 1, { ...array?.field?.value[index], disabled: !checked });
|
||||
}}
|
||||
onClick={(checked, e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
@ -0,0 +1,262 @@
|
||||
import React, { Fragment, useState, useEffect } from 'react';
|
||||
import { Badge, Card, Collapse, CollapsePanelProps, CollapseProps, Empty, Input } from 'antd';
|
||||
import { ArrayField } from '@formily/core';
|
||||
import { RecursionField, useField, useFieldSchema, observer, ISchema } from '@formily/react';
|
||||
import { toArr } from '@formily/shared';
|
||||
import cls from 'classnames';
|
||||
import { CopyOutlined } from '@ant-design/icons';
|
||||
import { clone } from 'lodash';
|
||||
import { ArrayBase, ArrayBaseMixins } from '@formily/antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const LinkageRulesTitle = (props) => {
|
||||
const array = ArrayBase.useArray();
|
||||
const index = ArrayBase.useIndex(props.index);
|
||||
const { t } = useTranslation();
|
||||
const value = array?.field?.value[index];
|
||||
return (
|
||||
<Input.TextArea
|
||||
value={value.title || t('linkage rule')}
|
||||
onChange={(ev) => {
|
||||
ev.stopPropagation();
|
||||
array.field.value.splice(index, 1, { ...value, title: ev.target.value });
|
||||
}}
|
||||
onBlur={(ev) => {
|
||||
ev.stopPropagation();
|
||||
array.field.value.splice(index, 1, { ...value, title: ev.target.value });
|
||||
}}
|
||||
autoSize
|
||||
style={{ width: '70%', border: 'none' }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export interface IArrayCollapseProps extends CollapseProps {
|
||||
defaultOpenPanelCount?: number;
|
||||
}
|
||||
type ComposedArrayCollapse = React.FC<React.PropsWithChildren<IArrayCollapseProps>> &
|
||||
ArrayBaseMixins & {
|
||||
CollapsePanel?: React.FC<React.PropsWithChildren<CollapsePanelProps>>;
|
||||
};
|
||||
|
||||
const isAdditionComponent = (schema: ISchema) => {
|
||||
return schema['x-component']?.indexOf?.('Addition') > -1;
|
||||
};
|
||||
|
||||
const isIndexComponent = (schema: ISchema) => {
|
||||
return schema['x-component']?.indexOf?.('Index') > -1;
|
||||
};
|
||||
|
||||
const isRemoveComponent = (schema: ISchema) => {
|
||||
return schema['x-component']?.indexOf?.('Remove') > -1;
|
||||
};
|
||||
|
||||
const isMoveUpComponent = (schema: ISchema) => {
|
||||
return schema['x-component']?.indexOf?.('MoveUp') > -1;
|
||||
};
|
||||
|
||||
const isMoveDownComponent = (schema: ISchema) => {
|
||||
return schema['x-component']?.indexOf?.('MoveDown') > -1;
|
||||
};
|
||||
const isCopyComponent = (schema: ISchema) => {
|
||||
return schema['x-component']?.indexOf?.('Copy') > -1;
|
||||
};
|
||||
const isOperationComponent = (schema: ISchema) => {
|
||||
return (
|
||||
isAdditionComponent(schema) ||
|
||||
isRemoveComponent(schema) ||
|
||||
isMoveDownComponent(schema) ||
|
||||
isMoveUpComponent(schema) ||
|
||||
isCopyComponent(schema)
|
||||
);
|
||||
};
|
||||
|
||||
const range = (count: number) => Array.from({ length: count }).map((_, i) => i);
|
||||
|
||||
const takeDefaultActiveKeys = (dataSourceLength: number, defaultOpenPanelCount: number) => {
|
||||
if (dataSourceLength < defaultOpenPanelCount) return range(dataSourceLength);
|
||||
return range(defaultOpenPanelCount);
|
||||
};
|
||||
|
||||
const insertActiveKeys = (activeKeys: number[], index: number) => {
|
||||
if (activeKeys.length <= index) return activeKeys.concat(index);
|
||||
return activeKeys.reduce((buf, key) => {
|
||||
if (key < index) return buf.concat(key);
|
||||
if (key === index) return buf.concat([key, key + 1]);
|
||||
return buf.concat(key + 1);
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const ArrayCollapse: ComposedArrayCollapse = observer((props: IArrayCollapseProps) => {
|
||||
const field = useField<ArrayField>();
|
||||
const dataSource = Array.isArray(field.value) ? field.value : [];
|
||||
const [activeKeys, setActiveKeys] = useState<number[]>(
|
||||
takeDefaultActiveKeys(dataSource.length, props.defaultOpenPanelCount),
|
||||
);
|
||||
const schema = useFieldSchema();
|
||||
const prefixCls = 'ant-formily-array-collapse';
|
||||
useEffect(() => {
|
||||
if (!field.modified && dataSource.length) {
|
||||
setActiveKeys(takeDefaultActiveKeys(dataSource.length, props.defaultOpenPanelCount));
|
||||
}
|
||||
}, [dataSource.length, field]);
|
||||
if (!schema) throw new Error('can not found schema object');
|
||||
|
||||
const renderAddition = () => {
|
||||
return schema.reduceProperties((addition, schema, key) => {
|
||||
if (isAdditionComponent(schema)) {
|
||||
return <RecursionField schema={schema} name={key} />;
|
||||
}
|
||||
return addition;
|
||||
}, null);
|
||||
};
|
||||
const renderEmpty = () => {
|
||||
if (dataSource.length) return;
|
||||
return (
|
||||
<Card className={cls(`${prefixCls}-item`, props.className)}>
|
||||
<Empty />
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const renderItems = () => {
|
||||
return (
|
||||
<Collapse
|
||||
{...props}
|
||||
activeKey={activeKeys}
|
||||
onChange={(keys: string[]) => setActiveKeys(toArr(keys).map(Number))}
|
||||
className={cls(`${prefixCls}-item`, props.className)}
|
||||
>
|
||||
{dataSource.map((item, index) => {
|
||||
const items = Array.isArray(schema.items) ? schema.items[index] || schema.items[0] : schema.items;
|
||||
|
||||
const panelProps = field.query(`${field.address}.${index}`).get('componentProps');
|
||||
const props: CollapsePanelProps = items['x-component-props'];
|
||||
const header = () => {
|
||||
const header = `${panelProps?.header || props.header || field.title}`;
|
||||
const path = field.address.concat(index);
|
||||
const errors = field.form.queryFeedbacks({
|
||||
type: 'error',
|
||||
address: `${path}.**`,
|
||||
});
|
||||
return (
|
||||
<ArrayBase.Item index={index} record={() => field.value?.[index]}>
|
||||
<RecursionField
|
||||
schema={items}
|
||||
name={index}
|
||||
filterProperties={(schema) => {
|
||||
if (!isIndexComponent(schema)) return false;
|
||||
return true;
|
||||
}}
|
||||
onlyRenderProperties
|
||||
/>
|
||||
{errors.length ? (
|
||||
<Badge size="small" className="errors-badge" count={errors.length}>
|
||||
{header}
|
||||
</Badge>
|
||||
) : (
|
||||
<LinkageRulesTitle item={item.initialValue || item} index={index} />
|
||||
)}
|
||||
</ArrayBase.Item>
|
||||
);
|
||||
};
|
||||
|
||||
const extra = (
|
||||
<ArrayBase.Item index={index} record={item}>
|
||||
<RecursionField
|
||||
schema={items}
|
||||
name={index}
|
||||
filterProperties={(schema) => {
|
||||
if (!isOperationComponent(schema)) return false;
|
||||
return true;
|
||||
}}
|
||||
onlyRenderProperties
|
||||
/>
|
||||
{panelProps?.extra}
|
||||
</ArrayBase.Item>
|
||||
);
|
||||
|
||||
const content = (
|
||||
<RecursionField
|
||||
schema={items}
|
||||
name={index}
|
||||
filterProperties={(schema) => {
|
||||
if (isIndexComponent(schema)) return false;
|
||||
if (isOperationComponent(schema)) return false;
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<Collapse.Panel {...props} {...panelProps} forceRender key={index} header={header()} extra={extra}>
|
||||
<ArrayBase.Item index={index} key={index} record={item}>
|
||||
{content}
|
||||
</ArrayBase.Item>
|
||||
</Collapse.Panel>
|
||||
);
|
||||
})}
|
||||
</Collapse>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<ArrayBase
|
||||
onAdd={(index) => {
|
||||
setActiveKeys(insertActiveKeys(activeKeys, index));
|
||||
}}
|
||||
>
|
||||
{renderEmpty()}
|
||||
{renderItems()}
|
||||
{renderAddition()}
|
||||
</ArrayBase>
|
||||
);
|
||||
});
|
||||
|
||||
const CollapsePanel: React.FC<React.PropsWithChildren<CollapsePanelProps>> = ({ children }) => {
|
||||
return <Fragment>{children}</Fragment>;
|
||||
};
|
||||
|
||||
CollapsePanel.displayName = 'CollapsePanel';
|
||||
|
||||
ArrayCollapse.defaultProps = {
|
||||
defaultOpenPanelCount: 5,
|
||||
};
|
||||
ArrayCollapse.displayName = 'ArrayCollapse';
|
||||
ArrayCollapse.CollapsePanel = CollapsePanel;
|
||||
|
||||
ArrayBase.mixin(ArrayCollapse);
|
||||
|
||||
export default ArrayCollapse;
|
||||
|
||||
//@ts-ignore
|
||||
ArrayCollapse.Copy = React.forwardRef((props: any, ref) => {
|
||||
const self = useField();
|
||||
const array = ArrayBase.useArray();
|
||||
const index = ArrayBase.useIndex(props.index);
|
||||
if (!array) return null;
|
||||
if (array.field?.pattern !== 'editable') return null;
|
||||
return (
|
||||
<CopyOutlined
|
||||
{...props}
|
||||
style={{
|
||||
transition: 'all 0.25s ease-in-out',
|
||||
color: 'rgba(0, 0, 0, 0.8)',
|
||||
fontSize: '16px',
|
||||
marginLeft: 6,
|
||||
}}
|
||||
ref={ref}
|
||||
onClick={(e) => {
|
||||
if (self?.disabled) return;
|
||||
e.stopPropagation();
|
||||
if (array.props?.disabled) return;
|
||||
const value = clone(array?.field?.value[index]);
|
||||
array.field.push(value);
|
||||
if (props.onClick) {
|
||||
props.onClick(e);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
@ -12,4 +12,4 @@ export interface FilterContextProps {
|
||||
|
||||
export const RemoveActionContext = createContext(null);
|
||||
export const FilterContext = createContext<FilterContextProps>(null);
|
||||
export const FilterLogicContext = createContext(null);
|
||||
export const LinkageLogicContext = createContext(null);
|
@ -5,6 +5,8 @@ import { SchemaComponent } from '../../schema-component';
|
||||
import { FilterContext } from '../../schema-component/antd/filter/context';
|
||||
import { FilterDynamicComponent } from './FilterDynamicComponent';
|
||||
import { LinkageRuleActionGroup } from './LinkageRuleActionGroup';
|
||||
import { ArrayCollapse } from './components/LinkageHeader';
|
||||
import { EnableLinkage } from './components/EnableLinkage';
|
||||
|
||||
export const FormLinkageRules = observer((props: any) => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
@ -13,6 +15,7 @@ export const FormLinkageRules = observer((props: any) => {
|
||||
return (
|
||||
<FilterContext.Provider value={{ field: options, fieldSchema, dynamicComponent, options: options || [] }}>
|
||||
<SchemaComponent
|
||||
components={{ ArrayCollapse }}
|
||||
schema={{
|
||||
type: 'object',
|
||||
properties: {
|
||||
@ -28,13 +31,9 @@ export const FormLinkageRules = observer((props: any) => {
|
||||
type: 'object',
|
||||
'x-component': 'ArrayCollapse.CollapsePanel',
|
||||
'x-component-props': {
|
||||
header: '{{ t("Linkage rule") }}',
|
||||
extra: [<EnableLinkage />],
|
||||
},
|
||||
properties: {
|
||||
index: {
|
||||
type: 'void',
|
||||
'x-component': 'ArrayCollapse.Index',
|
||||
},
|
||||
layout: {
|
||||
type: 'void',
|
||||
'x-component': 'FormLayout',
|
||||
@ -91,6 +90,10 @@ export const FormLinkageRules = observer((props: any) => {
|
||||
type: 'void',
|
||||
'x-component': 'ArrayCollapse.MoveDown',
|
||||
},
|
||||
copy: {
|
||||
type: 'void',
|
||||
'x-component': 'ArrayCollapse.Copy',
|
||||
},
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useField } from '@formily/react';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { FilterLogicContext } from './context';
|
||||
import { useEffect, useContext } from 'react';
|
||||
import { LinkageLogicContext } from './context';
|
||||
|
||||
const findOption = (dataIndex = [], options) => {
|
||||
let items = options;
|
||||
@ -17,11 +17,11 @@ const findOption = (dataIndex = [], options) => {
|
||||
|
||||
export const useValues = (options) => {
|
||||
const field = useField<any>();
|
||||
const logic = useContext(FilterLogicContext);
|
||||
const logic = useContext(LinkageLogicContext);
|
||||
|
||||
const value2data = () => {
|
||||
field.data = { ...field.initialValue };
|
||||
const dataIndex = field.initialValue?.targetFields;
|
||||
field.data = { ...(field.initialValue || field.value) };
|
||||
const dataIndex = field.data?.targetFields;
|
||||
const option = (dataIndex && findOption(dataIndex, options)) || {};
|
||||
const operators = option?.operators || [];
|
||||
field.data.operators = operators;
|
||||
@ -36,9 +36,6 @@ export const useValues = (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;
|
||||
|
@ -960,7 +960,9 @@ SchemaSettings.LinkageRules = (props) => {
|
||||
'x-component': FormLinkageRules,
|
||||
'x-component-props': {
|
||||
useProps: () => {
|
||||
const options = useCollectionFilterOptions(collectionName);
|
||||
const options = useCollectionFilterOptions(collectionName).filter(
|
||||
(v) => !['o2m', 'm2m'].includes(v.interface),
|
||||
);
|
||||
return {
|
||||
options,
|
||||
defaultValues: gridSchema?.['x-linkage-rules'] || fieldSchema?.['x-linkage-rules'],
|
||||
|
@ -15,7 +15,7 @@ function appendArrayColumn(scope, key) {
|
||||
if (Array.isArray(data) && !isIndex && !data[path]) {
|
||||
data[path] = data.map(item => item[path]);
|
||||
}
|
||||
data = data[path];
|
||||
data = data?.[path];
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user