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:
katherinehhh 2023-03-30 23:40:18 +08:00 committed by GitHub
parent 02758cb338
commit c2885ee1d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 490 additions and 162 deletions

View File

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

View File

@ -52,6 +52,7 @@ export default {
"Value":"Value",
"Disabled":"Disabled",
"Enabled":"Enabled",
"Empty":"Empty",
"Linkage rule":"Linkage rule",
"Linkage rules":"Linkage rules",
"Condition":"Condition",

View File

@ -58,6 +58,7 @@ export default {
"Value":"フィールド値",
"Disabled":"無効化",
"Enabled":"有効化",
"Empty":"くうきち",
"Linkage rule":"連動規則",
"Linkage rules":"連動規則",
"Condition":"条件#ジョウケン#",

View File

@ -50,6 +50,7 @@ export default {
"Value":"字段值",
"Disabled":"禁用",
"Enabled":"启用",
"Empty":"赋空值",
"Linkage rule":"联动规则",
"Linkage rules":"联动规则",
"Condition":"条件",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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();
}}
/>
);
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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