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) => { operators?.filter?.((operator) => {
return !operator?.visible || operator.visible(field); return !operator?.visible || operator.visible(field);
}) || [], }) || [],
interface: field.interface,
}; };
if (field.target && depth > 2) { if (field.target && depth > 2) {
return; return;

View File

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

View File

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

View File

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

View File

@ -2,13 +2,14 @@ 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, useEffect } from 'react'; import React, { useEffect, useState } from 'react';
import { useActionContext } from '../..'; import { useActionContext } from '../..';
import { useDesignable } from '../../';
import { Icon } from '../../../icon'; import { Icon } from '../../../icon';
import { useRecord } from '../../../record-provider';
import { SortableItem } from '../../common'; import { SortableItem } from '../../common';
import { useCompile, useComponent, useDesigner } from '../../hooks'; import { useCompile, useComponent, 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';
@ -18,7 +19,6 @@ 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'; import { linkageAction } from './utils';
export const actionDesignerCss = css` export const actionDesignerCss = css`
@ -96,11 +96,13 @@ export const Action: ComposedAction = observer((props: any) => {
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 linkageRules = fieldSchema?.['x-linkage-rules'] || [];
const { designable, } = useDesignable(); const { designable } = useDesignable();
const tarComponent = useComponent(component) || component; const tarComponent = useComponent(component) || component;
useEffect(() => { useEffect(() => {
field.linkageProperty = {}; field.linkageProperty = {};
linkageRules.map((v) => { linkageRules
.filter((k) => !k.disabled)
.map((v) => {
return v.actions?.map((h) => { return v.actions?.map((h) => {
linkageAction(h.operator, field, v.condition, values); linkageAction(h.operator, field, v.condition, values);
}); });

View File

@ -69,7 +69,8 @@ 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'] || []; const linkageRules =
(getLinkageRules(fieldSchema) || fieldSchema.parent?.['x-linkage-rules'])?.filter((k) => !k.disabled) || [];
useEffect(() => { useEffect(() => {
const id = uid(); const id = uid();
form.addEffects(id, () => { form.addEffects(id, () => {
@ -100,7 +101,9 @@ const WithForm = (props) => {
}; };
}); });
onFieldChange(`*(${fields})`, ['value', 'required', 'pattern', 'display'], (field: any) => { 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 { last, cloneDeep } from 'lodash';
import * as functions from '@formulajs/formulajs';
import { conditionAnalyse } from '../../common/utils/uitls'; import { conditionAnalyse } from '../../common/utils/uitls';
import { ActionType } from '../../../schema-settings/LinkageRules/type'; import { ActionType } from '../../../schema-settings/LinkageRules/type';
import evaluators from '@nocobase/evaluators/client';
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) => { export const linkageMergeAction = ({ operator, value }, field, condition, values) => {
const requiredResult = field?.linkageProperty?.required || [field?.initProperty?.required || false]; const requiredResult = field?.linkageProperty?.required || [field?.initProperty?.required || false];
const displayResult = field?.linkageProperty?.display || [field?.initProperty?.display]; const displayResult = field?.linkageProperty?.display || [field?.initProperty?.display];
const patternResult = field?.linkageProperty?.pattern || [field?.initProperty?.pattern]; const patternResult = field?.linkageProperty?.pattern || [field?.initProperty?.pattern];
const valueResult = field?.linkageProperty?.value || [field.value || field?.initProperty?.value]; const valueResult = field?.linkageProperty?.value || [field.value || field?.initProperty?.value];
const { evaluate } = evaluators.get('formula.js');
switch (operator) { switch (operator) {
case ActionType.Required: case ActionType.Required:
@ -91,17 +57,23 @@ export const linkageMergeAction = ({ operator, value }, field, condition, values
case ActionType.Value: case ActionType.Value:
if (conditionAnalyse(condition, values)) { if (conditionAnalyse(condition, values)) {
if (value?.mode === 'express') { if (value?.mode === 'express') {
const result = evaluate(value.result || value.value, values); const scope = cloneDeep(values);
try {
const result = evaluate(value.result || value.value, { ...scope, now: () => new Date().toString() });
valueResult.push(result); valueResult.push(result);
} else { } catch (error) {
}
} else if (value?.mode === 'constant') {
valueResult.push(value?.value || value); valueResult.push(value?.value || value);
} else {
valueResult.push(null);
} }
} }
field.linkageProperty = { field.linkageProperty = {
...field.linkageProperty, ...field.linkageProperty,
value: valueResult, value: valueResult,
}; };
setTimeout(() => (field.value = last(valueResult) === undefined ? field.value : last(valueResult))); field.value = last(valueResult) === undefined ? field.value : last(valueResult);
break; break;
default: default:
return null; return null;

View File

@ -39,7 +39,10 @@ http://ricostacruz.com/cheatsheets/umdjs.html
var jsonLogic = {}; var jsonLogic = {};
var operations = { var operations = {
$is: function (a, b) { $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) { $eq: function (a, b) {
return a === b; return a === b;
@ -91,6 +94,12 @@ http://ricostacruz.com/cheatsheets/umdjs.html
if (!a || typeof a.indexOf === 'undefined') return false; if (!a || typeof a.indexOf === 'undefined') return false;
return !(a.indexOf(b) !== -1); return !(a.indexOf(b) !== -1);
}, },
$isTruly: function (a) {
return a === true || a === 1;
},
$isFalsy: function (a) {
return !jsonLogic.truthy(a);
},
cat: function () { cat: function () {
return Array.prototype.join.call(arguments, ''); 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 flat from 'flat';
import jsonLogic from '../../common/utils/logic'; import jsonLogic from '../../common/utils/logic';
function getDeepestProperty(obj) { function getInnermostKeyAndValue(obj) {
let deepest = { if (typeof obj !== 'object' || obj === null) {
operator: null, return null;
value: null, }
};
let deepestLevel = 0;
function traverse(obj, level) {
for (const key in obj) { for (const key in obj) {
if (obj.hasOwnProperty(key)) { if (Object.prototype.hasOwnProperty.call(obj, key)) {
if (typeof obj[key] === 'object') { if (Object.prototype.toString.call(obj[key]) === '[object Object]' && obj[key] !== null) {
traverse(obj[key], level + 1); return getInnermostKeyAndValue(obj[key]);
} else { } else {
if (level > deepestLevel) { return { key, value: obj[key] };
deepestLevel = level;
deepest.operator = key;
deepest.value = obj[key];
} }
} }
} }
} return null;
}
traverse(obj, 1);
return deepest;
} }
const getValue = (str, values) => { const getValue = (str, values) => {
const regex = /{{(.*?)}}/; const regex = /{{(.*?)}}/;
@ -40,20 +31,47 @@ const getVariableValue = (str, values) => {
const match = regex.exec(str); const match = regex.exec(str);
return values[match?.[1]]; 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) => { export const conditionAnalyse = (rules, values) => {
const type = Object.keys(rules)[0] || '$and'; const type = Object.keys(rules)[0] || '$and';
const conditions = rules[type]; const conditions = rules[type];
const results = conditions.map((c) => { const results = conditions.map((c) => {
const jsonlogic = getDeepestProperty(c); const jsonlogic = getInnermostKeyAndValue(c);
const operator = jsonlogic.operator; const operator = jsonlogic?.key;
const value = getValue(jsonlogic.value, values); const value = getValue(jsonlogic?.value, values);
const targetField = Object.keys(flat(c))[0]?.replace?.(`.${operator}`, ''); const targetField = getTargetField(c);
if (!operator) { if (!operator) {
return true; return true;
} }
try { 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; return result;
} catch (error) { } catch (error) {
console.error(error); console.error(error);

View File

@ -1,12 +1,13 @@
import { CloseCircleOutlined } from '@ant-design/icons'; import { CloseCircleOutlined } from '@ant-design/icons';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { observer } from '@formily/react'; import { observer, useField } from '@formily/react';
import { TreeSelect } from '@formily/antd'; import { TreeSelect } from '@formily/antd';
import { Select, Space } from 'antd'; import { Select, Space } from 'antd';
import React, { useContext, useState } from 'react'; import React, { useContext, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { uid } from '@formily/shared';
import { useCompile } from '../..'; import { useCompile } from '../..';
import { RemoveActionContext } from './context'; import { RemoveActionContext, LinkageLogicContext } from './context';
import { DynamicComponent } from './DynamicComponent'; import { DynamicComponent } from './DynamicComponent';
import { useValues } from './useValues'; import { useValues } from './useValues';
import { ActionType } from './type'; import { ActionType } from './type';
@ -28,6 +29,7 @@ export const FormFieldLinkageRuleAction = observer((props: any) => {
operators, operators,
} = useValues(options); } = useValues(options);
return ( return (
<LinkageLogicContext.Provider value={uid()}>
<div style={{ marginBottom: 8 }}> <div style={{ marginBottom: 8 }}>
<Space <Space
className={css` className={css`
@ -51,7 +53,6 @@ export const FormFieldLinkageRuleAction = observer((props: any) => {
allowClear allowClear
treeData={compile(fields)} treeData={compile(fields)}
onChange={(value) => { onChange={(value) => {
console.log(value)
setDataIndex(value); setDataIndex(value);
}} }}
placeholder={t('Select Field')} placeholder={t('Select Field')}
@ -82,6 +83,7 @@ export const FormFieldLinkageRuleAction = observer((props: any) => {
)} )}
</Space> </Space>
</div> </div>
</LinkageLogicContext.Provider>
); );
}); });

View File

@ -1,25 +1,37 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Input, Select } from 'antd'; import { Input, Select } from 'antd';
import { css } from '@emotion/css';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { DynamicComponent } from './DynamicComponent'; import { DynamicComponent } from './DynamicComponent';
const { Option } = Select;
import { Variable } from '.././../schema-component'; import { Variable } from '.././../schema-component';
import { useVariableOptions } from './Variables'; import { useVariableOptions } from './Variables';
const { Option } = Select;
export const ValueDynamicComponent = (props) => { export const ValueDynamicComponent = (props) => {
const { fieldValue, schema, setValue, collectionName } = props; const { fieldValue, schema, setValue, collectionName } = props;
const [mode, setMode] = useState(fieldValue?.mode || 'constant'); const [mode, setMode] = useState(fieldValue?.mode || 'constant');
const { t } = useTranslation(); const { t } = useTranslation();
const scope = useVariableOptions(collectionName); const scope = useVariableOptions(collectionName);
return ( return (
<Input.Group compact style={{ minWidth: 280 }}> <Input.Group compact>
<Select value={mode} style={{ width: 150 }} onChange={(value) => setMode(value)}> <Select
value={mode}
style={{ width: 150 }}
onChange={(value) => {
setMode(value);
setValue({
mode: value,
});
}}
>
<Option value="constant">{t('Constant value')}</Option> <Option value="constant">{t('Constant value')}</Option>
<Option value="express">{t('Expression')}</Option> <Option value="express">{t('Expression')}</Option>
<Option value="empty">{t('Empty')}</Option>
</Select> </Select>
<div style={{ minWidth: 150, maxWidth: 430 }}>
{mode === 'constant' ? ( {mode === 'constant' ? (
React.createElement(DynamicComponent, { <div style={{ minWidth: 150, maxWidth: 430 }}>
{React.createElement(DynamicComponent, {
value: fieldValue?.value || fieldValue, value: fieldValue?.value || fieldValue,
schema, schema,
onChange(value) { onChange(value) {
@ -28,8 +40,10 @@ export const ValueDynamicComponent = (props) => {
value, value,
}); });
}, },
}) })}
) : ( </div>
) : mode === 'express' ? (
<div style={{ minWidth: 150, maxWidth: 430, fontSize: 13 }}>
<Variable.TextArea <Variable.TextArea
value={fieldValue?.value} value={fieldValue?.value}
onChange={(value) => { onChange={(value) => {
@ -43,8 +57,16 @@ export const ValueDynamicComponent = (props) => {
scope={scope} scope={scope}
style={{ minWidth: 460, marginRight: 15 }} style={{ minWidth: 460, marginRight: 15 }}
/> />
)} <>
<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> </div>
) : null}
</Input.Group> </Input.Group>
); );
}; };

View File

@ -17,9 +17,12 @@ const supportsType = [
'checkboxGroup', 'checkboxGroup',
'select', 'select',
'multipleSelect', 'multipleSelect',
'formula',
'oho', 'oho',
'obo', 'obo',
'm2o', 'm2o',
'o2m',
'm2m',
]; ];
const useVariableTypes = (currentCollection) => { const useVariableTypes = (currentCollection) => {
const { getCollectionFields, getInterface, getCollection } = useCollectionManager(); const { getCollectionFields, getInterface, getCollection } = useCollectionManager();
@ -45,10 +48,10 @@ const useVariableTypes = (currentCollection) => {
schema: field?.uiSchema, schema: field?.uiSchema,
value: field.name, value: field.name,
}; };
if (field.target && depth > 2) { if (field.target && depth > 1) {
return; return;
} }
if (depth > 2) { if (depth > 1) {
return option; return option;
} }
if (children?.length) { 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 RemoveActionContext = createContext(null);
export const FilterContext = createContext<FilterContextProps>(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 { FilterContext } from '../../schema-component/antd/filter/context';
import { FilterDynamicComponent } from './FilterDynamicComponent'; import { FilterDynamicComponent } from './FilterDynamicComponent';
import { LinkageRuleActionGroup } from './LinkageRuleActionGroup'; import { LinkageRuleActionGroup } from './LinkageRuleActionGroup';
import { ArrayCollapse } from './components/LinkageHeader';
import { EnableLinkage } from './components/EnableLinkage';
export const FormLinkageRules = observer((props: any) => { export const FormLinkageRules = observer((props: any) => {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
@ -13,6 +15,7 @@ export const FormLinkageRules = observer((props: any) => {
return ( return (
<FilterContext.Provider value={{ field: options, fieldSchema, dynamicComponent, options: options || [] }}> <FilterContext.Provider value={{ field: options, fieldSchema, dynamicComponent, options: options || [] }}>
<SchemaComponent <SchemaComponent
components={{ ArrayCollapse }}
schema={{ schema={{
type: 'object', type: 'object',
properties: { properties: {
@ -28,13 +31,9 @@ export const FormLinkageRules = observer((props: any) => {
type: 'object', type: 'object',
'x-component': 'ArrayCollapse.CollapsePanel', 'x-component': 'ArrayCollapse.CollapsePanel',
'x-component-props': { 'x-component-props': {
header: '{{ t("Linkage rule") }}', extra: [<EnableLinkage />],
}, },
properties: { properties: {
index: {
type: 'void',
'x-component': 'ArrayCollapse.Index',
},
layout: { layout: {
type: 'void', type: 'void',
'x-component': 'FormLayout', 'x-component': 'FormLayout',
@ -91,6 +90,10 @@ export const FormLinkageRules = observer((props: any) => {
type: 'void', type: 'void',
'x-component': 'ArrayCollapse.MoveDown', 'x-component': 'ArrayCollapse.MoveDown',
}, },
copy: {
type: 'void',
'x-component': 'ArrayCollapse.Copy',
},
}, },
}, },
properties: { properties: {

View File

@ -1,6 +1,6 @@
import { useField } from '@formily/react'; import { useField } from '@formily/react';
import { useContext, useEffect } from 'react'; import { useEffect, useContext } from 'react';
import { FilterLogicContext } from './context'; import { LinkageLogicContext } from './context';
const findOption = (dataIndex = [], options) => { const findOption = (dataIndex = [], options) => {
let items = options; let items = options;
@ -17,11 +17,11 @@ const findOption = (dataIndex = [], options) => {
export const useValues = (options) => { export const useValues = (options) => {
const field = useField<any>(); const field = useField<any>();
const logic = useContext(FilterLogicContext); const logic = useContext(LinkageLogicContext);
const value2data = () => { const value2data = () => {
field.data = { ...field.initialValue }; field.data = { ...(field.initialValue || field.value) };
const dataIndex = field.initialValue?.targetFields; const dataIndex = field.data?.targetFields;
const option = (dataIndex && findOption(dataIndex, options)) || {}; const option = (dataIndex && findOption(dataIndex, options)) || {};
const operators = option?.operators || []; const operators = option?.operators || [];
field.data.operators = operators; field.data.operators = operators;
@ -36,9 +36,6 @@ export const useValues = (options) => {
field.data = field.data || {}; field.data = field.data || {};
const operators = option?.operators; const operators = option?.operators;
field.data.operators = operators?.filter((v) => { field.data.operators = operators?.filter((v) => {
// if (dataIndex.length > 1) {
// return v.value !== 'value';
// }
return true; return true;
}); });
field.data.schema = option?.schema; field.data.schema = option?.schema;

View File

@ -960,7 +960,9 @@ SchemaSettings.LinkageRules = (props) => {
'x-component': FormLinkageRules, 'x-component': FormLinkageRules,
'x-component-props': { 'x-component-props': {
useProps: () => { useProps: () => {
const options = useCollectionFilterOptions(collectionName); const options = useCollectionFilterOptions(collectionName).filter(
(v) => !['o2m', 'm2m'].includes(v.interface),
);
return { return {
options, options,
defaultValues: gridSchema?.['x-linkage-rules'] || fieldSchema?.['x-linkage-rules'], 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]) { if (Array.isArray(data) && !isIndex && !data[path]) {
data[path] = data.map(item => item[path]); data[path] = data.map(item => item[path]);
} }
data = data[path]; data = data?.[path];
} }
} }