refactor(client): refactor variable components and variables in workflow (#2157)
* refactor(plugin-workflow): change collection variables to lazy load * fix(plugin-workflow): avoid to-many reverse loading for association field * fix(client): fix variable components * chore(client): fix type * fix(client): fix current user lazy load options * refactor(client): remove compile from variable components which potencially causing bug * fix(plugin-workflow): fix scope argument for new api * fix(client): fix constant type options * fix(client): fix infinity rerendering * fix: avoid closure problem * fix(client): should use no children when lazy load * refactor(client): refactor AssignedField to use Variable component * fix(client): fix type * fix(plugin-workflow): fix variable options in some node not changes * fix(plugin-workflow): fix select variable for operand crash (T-815) * fix(plugin-workflow): variable types detect * fix(plugin-workflow): detect association to match types * fix(plugin-workflow): fix variable type filter logic * fix(plugin-workflow): fix optional types * fix(plugin-workflow): make changeOnSelect configurable in TextArea and JSONInput --------- Co-authored-by: Rairn <958414905@qq.com>
This commit is contained in:
parent
25a3a8affa
commit
c9b726916c
@ -2,19 +2,23 @@ import { CloseCircleFilled } from '@ant-design/icons';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { useForm } from '@formily/react';
|
||||
import { error } from '@nocobase/utils/client';
|
||||
import { Input as AntInput, Cascader, DatePicker, InputNumber, Select, Tag } from 'antd';
|
||||
import { Cascader, DatePicker, Input as AntInput, InputNumber, Select, Tag } from 'antd';
|
||||
import type { DefaultOptionType } from 'antd/lib/cascader';
|
||||
import classNames from 'classnames';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCompile } from '../..';
|
||||
import { Option } from '../../../schema-settings/VariableInput/type';
|
||||
import { useCompile } from '../../hooks';
|
||||
import { XButton } from './XButton';
|
||||
|
||||
const JT_VALUE_RE = /^\s*{{\s*([^{}]+)\s*}}\s*$/;
|
||||
const groupClass = css`
|
||||
width: auto;
|
||||
display: flex !important;
|
||||
display: flex;
|
||||
&.ant-input-group-compact {
|
||||
display: flex;
|
||||
}
|
||||
.ant-input-disabled {
|
||||
.ant-tag {
|
||||
color: #bfbfbf;
|
||||
@ -115,69 +119,93 @@ const ConstantTypes = {
|
||||
},
|
||||
};
|
||||
|
||||
function getTypedConstantOption(type: string, types?: true | string[]) {
|
||||
function getTypedConstantOption(type: string, types: true | string[], fieldNames) {
|
||||
const allTypes = Object.values(ConstantTypes);
|
||||
const children = (
|
||||
types ? allTypes.filter((item) => (Array.isArray(types) && types.includes(item.value)) || types === true) : allTypes
|
||||
).map((item) =>
|
||||
Object.keys(item).reduce(
|
||||
(result, key) =>
|
||||
fieldNames[key] in item
|
||||
? result
|
||||
: Object.assign(result, {
|
||||
[fieldNames[key]]: item[key],
|
||||
}),
|
||||
item,
|
||||
),
|
||||
);
|
||||
return {
|
||||
value: '',
|
||||
label: '{{t("Constant")}}',
|
||||
children: types
|
||||
? allTypes.filter((item) => (Array.isArray(types) && types.includes(item.value)) || types === true)
|
||||
: allTypes,
|
||||
children,
|
||||
[fieldNames.value]: '',
|
||||
[fieldNames.label]: '{{t("Constant")}}',
|
||||
[fieldNames.children]: children,
|
||||
component: ConstantTypes[type]?.component,
|
||||
};
|
||||
}
|
||||
|
||||
type VariableOptions = {
|
||||
value: string;
|
||||
label?: string;
|
||||
children?: VariableOptions[];
|
||||
};
|
||||
|
||||
export function Input(props) {
|
||||
const {
|
||||
value = '',
|
||||
scope,
|
||||
onChange,
|
||||
children,
|
||||
button,
|
||||
useTypedConstant,
|
||||
style,
|
||||
className,
|
||||
changeOnSelect,
|
||||
fieldNames,
|
||||
} = props;
|
||||
const compile = useCompile();
|
||||
const { t } = useTranslation();
|
||||
const form = useForm();
|
||||
const [options, setOptions] = React.useState<DefaultOptionType[]>([]);
|
||||
const [variableText, setVariableText] = React.useState('');
|
||||
|
||||
const { value = '', scope, onChange, children, button, useTypedConstant, style, className } = props;
|
||||
const parsed = useMemo(() => parseValue(value), [value]);
|
||||
const isConstant = typeof parsed === 'string';
|
||||
const type = isConstant ? parsed : '';
|
||||
const variable = isConstant ? null : parsed;
|
||||
const names = Object.assign(
|
||||
{
|
||||
label: 'label',
|
||||
value: 'value',
|
||||
children: 'children',
|
||||
},
|
||||
fieldNames ?? {},
|
||||
);
|
||||
|
||||
// 当 scope 是一个函数时,可能是一个 hook,所以不能使用 useMemo
|
||||
const variableOptions = typeof scope === 'function' ? scope() : scope ?? [];
|
||||
|
||||
const [variableText, setVariableText] = React.useState('');
|
||||
|
||||
const { component: ConstantComponent, ...constantOption }: VariableOptions & { component?: React.FC<any> } =
|
||||
const { component: ConstantComponent, ...constantOption }: DefaultOptionType & { component?: React.FC<any> } =
|
||||
useMemo(() => {
|
||||
if (children) {
|
||||
return {
|
||||
value: '',
|
||||
label: '{{t("Constant")}}',
|
||||
label: t('Constant'),
|
||||
[names.value]: '',
|
||||
[names.label]: t('Constant'),
|
||||
};
|
||||
}
|
||||
if (useTypedConstant) {
|
||||
return getTypedConstantOption(type, useTypedConstant);
|
||||
return getTypedConstantOption(type, useTypedConstant, names);
|
||||
}
|
||||
return {
|
||||
value: '',
|
||||
label: '{{t("Null")}}',
|
||||
label: t('Null'),
|
||||
[names.value]: '',
|
||||
[names.label]: t('Null'),
|
||||
component: ConstantTypes.null.component,
|
||||
};
|
||||
}, [type, useTypedConstant]);
|
||||
|
||||
const [options, setOptions] = React.useState<Option[]>(() => {
|
||||
return compile([constantOption, ...variableOptions]);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const newOptions: Option[] = [constantOption, ...variableOptions];
|
||||
setOptions(deepCompileLabel(newOptions, compile));
|
||||
}, [variableOptions]);
|
||||
setOptions([compile(constantOption), ...(scope ? cloneDeep(scope) : [])]);
|
||||
}, [scope]);
|
||||
|
||||
const loadData = async (selectedOptions: Option[]) => {
|
||||
const loadData = async (selectedOptions: DefaultOptionType[]) => {
|
||||
const option = selectedOptions[selectedOptions.length - 1];
|
||||
if (option.loadChildren) {
|
||||
if (!option.children && !option.isLeaf && option.loadChildren) {
|
||||
await option.loadChildren(option);
|
||||
setOptions((prev) => [...prev]);
|
||||
}
|
||||
@ -199,29 +227,29 @@ export function Input(props) {
|
||||
}
|
||||
onChange(`{{${next.join('.')}}}`);
|
||||
},
|
||||
[type, variable],
|
||||
[type, variable, onChange],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const run = async () => {
|
||||
if (!variable || options.length <= 1) {
|
||||
if (!variable || !options.length) {
|
||||
return;
|
||||
}
|
||||
let prevOption: Option = null;
|
||||
let prevOption: DefaultOptionType = null;
|
||||
const labels = [];
|
||||
|
||||
for (let i = 0; i < variable.length; i++) {
|
||||
const key = variable[i];
|
||||
try {
|
||||
if (i === 0) {
|
||||
prevOption = options.find((item) => item.value === key);
|
||||
prevOption = options.find((item) => item[names.value] === key);
|
||||
} else {
|
||||
if (prevOption.loadChildren && !prevOption.children?.length) {
|
||||
await prevOption.loadChildren(prevOption);
|
||||
}
|
||||
prevOption = prevOption.children.find((item) => item.value === key);
|
||||
prevOption = prevOption.children.find((item) => item[names.value] === key);
|
||||
}
|
||||
labels.push(prevOption.label);
|
||||
labels.push(prevOption[names.label]);
|
||||
} catch (err) {
|
||||
error(err);
|
||||
}
|
||||
@ -230,17 +258,19 @@ export function Input(props) {
|
||||
setVariableText(labels.join(' / '));
|
||||
};
|
||||
|
||||
// 如果没有这个延迟,会导致选择父节点时不展开子节点
|
||||
setTimeout(run);
|
||||
run();
|
||||
// NOTE: watch `options.length` and it only happens once
|
||||
}, [variable, options.length]);
|
||||
|
||||
const disabled = props.disabled || form.disabled;
|
||||
|
||||
return (
|
||||
<AntInput.Group compact style={style} className={classNames(className, groupClass)}>
|
||||
<AntInput.Group compact style={style} className={classNames(groupClass, className)}>
|
||||
{variable ? (
|
||||
<div
|
||||
className={css`
|
||||
className={cx(
|
||||
'variable',
|
||||
css`
|
||||
position: relative;
|
||||
line-height: 0;
|
||||
|
||||
@ -263,7 +293,8 @@ export function Input(props) {
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
`}
|
||||
`,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
onInput={(e) => e.preventDefault()}
|
||||
@ -297,13 +328,14 @@ export function Input(props) {
|
||||
) : (
|
||||
children ?? <ConstantComponent value={value} onChange={onChange} />
|
||||
)}
|
||||
{options.length > 1 ? (
|
||||
{options.length > 1 && !disabled ? (
|
||||
<Cascader
|
||||
options={options}
|
||||
value={variable ?? ['', ...(children || !constantOption.children?.length ? [] : [type])]}
|
||||
onChange={onSwitch}
|
||||
loadData={loadData as any}
|
||||
changeOnSelect
|
||||
changeOnSelect={changeOnSelect}
|
||||
fieldNames={fieldNames}
|
||||
>
|
||||
{button ?? <XButton type={variable ? 'primary' : 'default'} />}
|
||||
</Cascader>
|
||||
@ -311,14 +343,3 @@ export function Input(props) {
|
||||
</AntInput.Group>
|
||||
);
|
||||
}
|
||||
|
||||
function deepCompileLabel(list: Option[], compile) {
|
||||
return list.map((item) => {
|
||||
const children = item.children ? deepCompileLabel(item.children, compile) : null;
|
||||
return {
|
||||
...item,
|
||||
label: compile(item.label),
|
||||
children,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React, { useRef } from 'react';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { Button } from 'antd';
|
||||
import { css } from '@emotion/css';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
import { Input } from '../input';
|
||||
import { VariableSelect } from './VariableSelect';
|
||||
@ -18,8 +19,8 @@ function setNativeInputValue(input, value) {
|
||||
|
||||
export function JSONInput(props) {
|
||||
const inputRef = useRef<any>(null);
|
||||
const { scope } = props;
|
||||
const options = typeof scope === 'function' ? scope() : scope ?? [];
|
||||
const { scope, changeOnSelect, ...others } = props;
|
||||
const [options, setOptions] = useState(scope ? cloneDeep(scope) : []);
|
||||
|
||||
function onInsert(selected) {
|
||||
if (!inputRef.current) {
|
||||
@ -45,7 +46,7 @@ export function JSONInput(props) {
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Input.JSON {...props} ref={inputRef} />
|
||||
<Input.JSON {...others} ref={inputRef} />
|
||||
<Button.Group
|
||||
className={css`
|
||||
position: absolute;
|
||||
@ -56,7 +57,7 @@ export function JSONInput(props) {
|
||||
}
|
||||
`}
|
||||
>
|
||||
<VariableSelect options={options} onInsert={onInsert} />
|
||||
<VariableSelect options={options} setOptions={setOptions} onInsert={onInsert} changeOnSelect={changeOnSelect} />
|
||||
</Button.Group>
|
||||
</div>
|
||||
);
|
||||
|
@ -2,10 +2,12 @@ import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { Input } from 'antd';
|
||||
import { useForm } from '@formily/react';
|
||||
import { cx, css } from '@emotion/css';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import * as sanitizeHTML from 'sanitize-html';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
import { EllipsisWithTooltip, useCompile } from '../..';
|
||||
import { error } from '@nocobase/utils/client';
|
||||
|
||||
import { EllipsisWithTooltip } from '../..';
|
||||
import { VariableSelect } from './VariableSelect';
|
||||
|
||||
type RangeIndexes = [number, number, number, number];
|
||||
@ -112,9 +114,9 @@ function createOptionsValueLabelMap(options: any[]) {
|
||||
|
||||
function createVariableTagHTML(variable, keyLabelMap) {
|
||||
const labels = keyLabelMap.get(variable);
|
||||
return `<span class="ant-tag ant-tag-blue" contentEditable="false" data-variable="${variable}">${labels?.join(
|
||||
' / ',
|
||||
)}</span>`;
|
||||
return `<span class="ant-tag ant-tag-blue" contentEditable="false" data-variable="${variable}">${
|
||||
labels ? labels.join(' / ') : '...'
|
||||
}</span>`;
|
||||
}
|
||||
|
||||
// [#, <>, #, #, <>]
|
||||
@ -185,29 +187,31 @@ function getCurrentRange(element: HTMLElement): RangeIndexes {
|
||||
}
|
||||
|
||||
export function TextArea(props) {
|
||||
const { value = '', scope, onChange, multiline = true } = props;
|
||||
const compile = useCompile();
|
||||
const { value = '', scope, onChange, multiline = true, changeOnSelect } = props;
|
||||
const inputRef = useRef<HTMLDivElement>(null);
|
||||
const options = compile((typeof scope === 'function' ? scope() : scope) ?? []);
|
||||
const [options, setOptions] = useState([]);
|
||||
const form = useForm();
|
||||
const keyLabelMap = useMemo(() => createOptionsValueLabelMap(options), [scope]);
|
||||
const keyLabelMap = useMemo(() => createOptionsValueLabelMap(options), [options]);
|
||||
const [ime, setIME] = useState<boolean>(false);
|
||||
const [changed, setChanged] = useState(false);
|
||||
const [html, setHtml] = useState(() => renderHTML(value ?? '', keyLabelMap));
|
||||
// NOTE: e.g. [startElementIndex, startOffset, endElementIndex, endOffset]
|
||||
const [range, setRange] = useState<[number, number, number, number]>([-1, 0, -1, 0]);
|
||||
const [selectedVar, setSelectedVar] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedVar([]);
|
||||
}, [scope]);
|
||||
preloadOptions(scope, value)
|
||||
.then((preloaded) => {
|
||||
setOptions(preloaded);
|
||||
})
|
||||
.catch((err) => console.error);
|
||||
}, [scope, value]);
|
||||
|
||||
useEffect(() => {
|
||||
setHtml(renderHTML(value ?? '', keyLabelMap));
|
||||
if (!changed) {
|
||||
setRange([-1, 0, -1, 0]);
|
||||
}
|
||||
}, [value]);
|
||||
}, [value, keyLabelMap]);
|
||||
|
||||
useEffect(() => {
|
||||
const { current } = inputRef;
|
||||
@ -375,16 +379,51 @@ export function TextArea(props) {
|
||||
contentEditable={!disabled}
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
{!disabled ? <VariableSelect options={options} onInsert={onInsert} /> : null}
|
||||
{!disabled ? (
|
||||
<VariableSelect options={options} setOptions={setOptions} onInsert={onInsert} changeOnSelect={changeOnSelect} />
|
||||
) : null}
|
||||
</Input.Group>
|
||||
);
|
||||
}
|
||||
|
||||
async function preloadOptions(scope, value) {
|
||||
const options = cloneDeep(scope ?? []);
|
||||
for (let matcher; (matcher = VARIABLE_RE.exec(value ?? '')); ) {
|
||||
const keys = matcher[1].split('.');
|
||||
|
||||
let prevOption = null;
|
||||
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
try {
|
||||
if (i === 0) {
|
||||
prevOption = options.find((item) => item.value === key);
|
||||
} else {
|
||||
if (prevOption.loadChildren && !prevOption.children?.length) {
|
||||
await prevOption.loadChildren(prevOption);
|
||||
}
|
||||
prevOption = prevOption.children.find((item) => item.value === key);
|
||||
}
|
||||
} catch (err) {
|
||||
error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
TextArea.ReadPretty = function ReadPretty(props): JSX.Element {
|
||||
const { value, multiline = true, scope } = props;
|
||||
const compile = useCompile();
|
||||
const options = compile((typeof scope === 'function' ? scope() : scope) ?? []);
|
||||
const keyLabelMap = useMemo(() => createOptionsValueLabelMap(options), [scope]);
|
||||
const { value, scope } = props;
|
||||
const [options, setOptions] = useState([]);
|
||||
const keyLabelMap = useMemo(() => createOptionsValueLabelMap(options), [options]);
|
||||
|
||||
useEffect(() => {
|
||||
preloadOptions(scope, value)
|
||||
.then((preloaded) => {
|
||||
setOptions(preloaded);
|
||||
})
|
||||
.catch(error);
|
||||
}, [scope, value]);
|
||||
const html = renderHTML(value ?? '', keyLabelMap);
|
||||
|
||||
const content = (
|
||||
|
@ -1,16 +1,19 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { Button, Cascader } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function VariableSelect(props) {
|
||||
const { options, onInsert } = props;
|
||||
export function VariableSelect({ options, setOptions, onInsert, changeOnSelect = false }): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const [selectedVar, setSelectedVar] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedVar([]);
|
||||
}, [options]);
|
||||
async function loadData(selectedOptions) {
|
||||
const option = selectedOptions[selectedOptions.length - 1];
|
||||
if (!option.children && !option.isLeaf && option.loadChildren) {
|
||||
await option.loadChildren(option);
|
||||
setOptions((prev) => [...prev]);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
@ -44,6 +47,7 @@ export function VariableSelect(props) {
|
||||
placeholder={t('Select a variable')}
|
||||
value={[]}
|
||||
options={options}
|
||||
loadData={loadData}
|
||||
onChange={(keyPaths = [], selectedOptions = []) => {
|
||||
setSelectedVar(keyPaths as string[]);
|
||||
if (!keyPaths.length) {
|
||||
@ -54,9 +58,9 @@ export function VariableSelect(props) {
|
||||
onInsert(keyPaths);
|
||||
}
|
||||
}}
|
||||
changeOnSelect
|
||||
changeOnSelect={changeOnSelect}
|
||||
onClick={(e: any) => {
|
||||
if (e.detail !== 2) {
|
||||
if (e.detail !== 2 || !changeOnSelect) {
|
||||
return;
|
||||
}
|
||||
for (let n = e.target; n && n !== e.currentTarget; n = n.parentNode) {
|
||||
@ -70,7 +74,9 @@ export function VariableSelect(props) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
`}
|
||||
dropdownRender={(menu) => (
|
||||
dropdownRender={
|
||||
changeOnSelect
|
||||
? (menu) => (
|
||||
<>
|
||||
{menu}
|
||||
<div
|
||||
@ -83,7 +89,9 @@ export function VariableSelect(props) {
|
||||
{t('Double click to choose entire object')}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
)
|
||||
: null
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { Field } from '@formily/core';
|
||||
import { connect, useField, useFieldSchema } from '@formily/react';
|
||||
import { merge } from '@formily/shared';
|
||||
import { Cascader, Select, Space } from 'antd';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useFormBlockContext } from '../../../block-provider';
|
||||
import {
|
||||
@ -11,17 +10,14 @@ import {
|
||||
useCollectionField,
|
||||
useCollectionFilterOptions,
|
||||
} from '../../../collection-manager';
|
||||
import { useCompile, useComponent } from '../../../schema-component';
|
||||
import { Variable, useCompile, useComponent } from '../../../schema-component';
|
||||
import { DeletedField } from '../DeletedField';
|
||||
|
||||
const DYNAMIC_RECORD_REG = /\{\{\s*currentRecord\.(.*)\s*\}\}/;
|
||||
const DYNAMIC_USER_REG = /\{\{\s*currentUser\.(.*)\s*\}\}/;
|
||||
const DYNAMIC_TIME_REG = /\{\{\s*currentTime\s*\}\}/;
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
const InternalField: React.FC = (props) => {
|
||||
const field = useField<Field>();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { name, interface: interfaceType, uiSchema } = useCollectionField();
|
||||
const { uiSchema } = useCollectionField();
|
||||
const component = useComponent(uiSchema?.['x-component']);
|
||||
const compile = useCompile();
|
||||
const setFieldProps = (key, value) => {
|
||||
@ -87,162 +83,55 @@ export enum AssignedFieldValueType {
|
||||
}
|
||||
|
||||
export const AssignedField = (props: any) => {
|
||||
const { value, onChange } = props;
|
||||
const { t } = useTranslation();
|
||||
const compile = useCompile();
|
||||
const collection = useCollection();
|
||||
const field = useField<Field>();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const isDynamicValue =
|
||||
DYNAMIC_RECORD_REG.test(field.value) || DYNAMIC_USER_REG.test(field.value) || DYNAMIC_TIME_REG.test(field.value);
|
||||
const initType = isDynamicValue ? AssignedFieldValueType.DynamicValue : AssignedFieldValueType.ConstantValue;
|
||||
const [type, setType] = useState<string>(initType);
|
||||
const initFieldType = {
|
||||
[`${DYNAMIC_TIME_REG.test(field.value)}`]: 'currentTime',
|
||||
[`${DYNAMIC_USER_REG.test(field.value)}`]: 'currentUser',
|
||||
[`${DYNAMIC_RECORD_REG.test(field.value)}`]: 'currentRecord',
|
||||
};
|
||||
const [fieldType, setFieldType] = useState<string>(initFieldType['true']);
|
||||
const initRecordValue = DYNAMIC_RECORD_REG.exec(field.value)?.[1]?.split('.') ?? [];
|
||||
const [recordValue, setRecordValue] = useState<any>(initRecordValue);
|
||||
const initUserValue = DYNAMIC_USER_REG.exec(field.value)?.[1]?.split('.') ?? [];
|
||||
const [userValue, setUserValue] = useState<any>(initUserValue);
|
||||
const initValue = isDynamicValue ? '' : field.value;
|
||||
const [value, setValue] = useState(initValue);
|
||||
const [options, setOptions] = useState<any[]>([]);
|
||||
const { getField } = useCollection();
|
||||
const collectionField = getField(fieldSchema.name);
|
||||
const fields = useCollectionFilterOptions(collection?.name);
|
||||
const userFields = useCollectionFilterOptions('users');
|
||||
const dateTimeFields = ['createdAt', 'datetime', 'time', 'updatedAt'];
|
||||
const [options, setOptions] = useState<any[]>([]);
|
||||
const collection = useCollection();
|
||||
const fields = compile(useCollectionFilterOptions(collection?.name));
|
||||
const userFields = compile(useCollectionFilterOptions('users'));
|
||||
useEffect(() => {
|
||||
const opt = [
|
||||
{
|
||||
name: 'currentRecord',
|
||||
title: t('Current record'),
|
||||
children: fields,
|
||||
},
|
||||
{
|
||||
name: 'currentUser',
|
||||
title: t('Current user'),
|
||||
children: userFields,
|
||||
},
|
||||
];
|
||||
if (dateTimeFields.includes(collectionField?.interface)) {
|
||||
if (['createdAt', 'datetime', 'time', 'updatedAt'].includes(collectionField?.interface)) {
|
||||
opt.unshift({
|
||||
name: 'currentTime',
|
||||
title: t('Current time'),
|
||||
children: null,
|
||||
});
|
||||
}
|
||||
setOptions(compile(opt));
|
||||
}, []);
|
||||
setOptions(opt);
|
||||
}, [fields, userFields]);
|
||||
|
||||
useEffect(() => {
|
||||
if (type === AssignedFieldValueType.ConstantValue) {
|
||||
field.value = value;
|
||||
} else {
|
||||
if (fieldType === 'currentTime') {
|
||||
field.value = '{{currentTime}}';
|
||||
} else if (fieldType === 'currentUser') {
|
||||
userValue?.length > 0 && (field.value = `{{currentUser.${userValue.join('.')}}}`);
|
||||
} else if (fieldType === 'currentRecord') {
|
||||
recordValue?.length > 0 && (field.value = `{{currentRecord.${recordValue.join('.')}}}`);
|
||||
}
|
||||
}
|
||||
}, [type, value, fieldType, userValue, recordValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (type === AssignedFieldValueType.ConstantValue) {
|
||||
setFieldType(null);
|
||||
setUserValue([]);
|
||||
setRecordValue([]);
|
||||
}
|
||||
}, [type]);
|
||||
|
||||
const typeChangeHandler = (val) => {
|
||||
setType(val);
|
||||
if (val === AssignedFieldValueType.DynamicValue) {
|
||||
field.validator = null;
|
||||
field.form.clearErrors();
|
||||
}
|
||||
};
|
||||
|
||||
const valueChangeHandler = (val) => {
|
||||
setValue(val?.target?.value ?? val);
|
||||
};
|
||||
|
||||
const fieldTypeChangeHandler = (val) => {
|
||||
setFieldType(val);
|
||||
};
|
||||
const recordChangeHandler = (val) => {
|
||||
setRecordValue(val);
|
||||
};
|
||||
const userChangeHandler = (val) => {
|
||||
setUserValue(val);
|
||||
};
|
||||
const useFieldMemo = useMemo(() => {
|
||||
if (!collectionField) {
|
||||
return <DeletedField />;
|
||||
}
|
||||
if (type === AssignedFieldValueType.ConstantValue) {
|
||||
return <CollectionField {...props} value={value} onChange={valueChangeHandler} style={{ minWidth: 150 }} />;
|
||||
} else {
|
||||
return (
|
||||
<Select
|
||||
dropdownMatchSelectWidth={false}
|
||||
defaultValue={fieldType}
|
||||
value={fieldType}
|
||||
style={{ minWidth: 150 }}
|
||||
onChange={fieldTypeChangeHandler}
|
||||
<Variable.Input
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
scope={options}
|
||||
className={css`
|
||||
.variable {
|
||||
width: 100%;
|
||||
}
|
||||
`}
|
||||
fieldNames={{
|
||||
label: 'title',
|
||||
value: 'name',
|
||||
}}
|
||||
>
|
||||
{options?.map((opt) => {
|
||||
return (
|
||||
<Select.Option key={opt.name} value={opt.name}>
|
||||
{opt.title}
|
||||
</Select.Option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
}, [collectionField, type, value, fieldType, options]);
|
||||
|
||||
return (
|
||||
<Space>
|
||||
<Select defaultValue={type} value={type} style={{ width: 150 }} onChange={typeChangeHandler}>
|
||||
<Select.Option value={AssignedFieldValueType.ConstantValue}>{t('Constant value')}</Select.Option>
|
||||
<Select.Option value={AssignedFieldValueType.DynamicValue}>{t('Dynamic value')}</Select.Option>
|
||||
</Select>
|
||||
|
||||
{useFieldMemo}
|
||||
|
||||
{fieldType === 'currentRecord' && (
|
||||
<Cascader
|
||||
fieldNames={{
|
||||
label: 'title',
|
||||
value: 'name',
|
||||
children: 'children',
|
||||
}}
|
||||
style={{
|
||||
minWidth: 150,
|
||||
}}
|
||||
options={compile(fields)}
|
||||
onChange={recordChangeHandler}
|
||||
defaultValue={recordValue}
|
||||
/>
|
||||
)}
|
||||
{fieldType === 'currentUser' && (
|
||||
<Cascader
|
||||
fieldNames={{
|
||||
label: 'title',
|
||||
value: 'name',
|
||||
children: 'children',
|
||||
}}
|
||||
style={{
|
||||
minWidth: 150,
|
||||
}}
|
||||
options={compile(userFields)}
|
||||
onChange={userChangeHandler}
|
||||
defaultValue={userValue}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
<CollectionField value={value} onChange={onChange} />
|
||||
</Variable.Input>
|
||||
);
|
||||
};
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useCompile, useGetFilterOptions } from '../../../schema-component';
|
||||
import { FieldOption, Option } from '../type';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface GetOptionsParams {
|
||||
depth: number;
|
||||
@ -35,7 +36,6 @@ const getChildren = (
|
||||
key: option.name,
|
||||
value: option.name,
|
||||
label: compile(option.title),
|
||||
children: [],
|
||||
isLeaf: false,
|
||||
field: option,
|
||||
depth,
|
||||
@ -57,6 +57,7 @@ export const useFormVariable = ({
|
||||
level?: number;
|
||||
}) => {
|
||||
const compile = useCompile();
|
||||
const { t } = useTranslation();
|
||||
const getFilterOptions = useGetFilterOptions();
|
||||
const loadChildren = (option: any): Promise<void> => {
|
||||
if (!option.field?.target) {
|
||||
@ -94,13 +95,15 @@ export const useFormVariable = ({
|
||||
}, 5);
|
||||
});
|
||||
};
|
||||
|
||||
const label = t('Current form');
|
||||
|
||||
const result = useMemo(() => {
|
||||
return (
|
||||
blockForm && {
|
||||
label: `{{t("Current form")}}`,
|
||||
label,
|
||||
value: '$form',
|
||||
key: '$form',
|
||||
children: [],
|
||||
isLeaf: false,
|
||||
field: {
|
||||
target: rootCollection,
|
||||
|
@ -38,7 +38,6 @@ const getChildren = (
|
||||
key: option.name,
|
||||
value: option.name,
|
||||
label: compile(option.title),
|
||||
children: [],
|
||||
isLeaf: false,
|
||||
field: option,
|
||||
depth,
|
||||
@ -93,7 +92,6 @@ export const useUserVariable = ({ schema, maxDepth = 3 }: { schema: any; maxDept
|
||||
label: t('Current user'),
|
||||
value: '$user',
|
||||
key: '$user',
|
||||
children: [],
|
||||
isLeaf: false,
|
||||
field: {
|
||||
target: 'users',
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { Schema } from '@formily/react';
|
||||
import type { DefaultOptionType } from 'antd/lib/cascader';
|
||||
|
||||
export interface Option {
|
||||
export interface Option extends DefaultOptionType {
|
||||
key?: string | number;
|
||||
value?: string | number;
|
||||
label?: React.ReactNode;
|
||||
label: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
children?: Option[];
|
||||
// 标记是否为叶子节点,设置了 `loadData` 时有效
|
||||
|
@ -4,16 +4,18 @@ import { Tag } from 'antd';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useRecord, Variable } from '@nocobase/client';
|
||||
import { useCollectionManager, useCompile, useRecord, Variable } from '@nocobase/client';
|
||||
|
||||
import { NAMESPACE } from '../locale';
|
||||
import { useCollectionFieldOptions } from '../variable';
|
||||
import { getCollectionFieldOptions } from '../variable';
|
||||
|
||||
const InternalExpression = observer(
|
||||
(props: any) => {
|
||||
const { onChange } = props;
|
||||
const { values } = useForm();
|
||||
const [collection, setCollection] = useState(values?.sourceCollection);
|
||||
const compile = useCompile();
|
||||
const { getCollectionFields } = useCollectionManager();
|
||||
|
||||
useFormEffects(() => {
|
||||
onFormInitialValuesChange((form) => {
|
||||
@ -25,7 +27,7 @@ const InternalExpression = observer(
|
||||
});
|
||||
});
|
||||
|
||||
const options = useCollectionFieldOptions({ collection: collection });
|
||||
const options = getCollectionFieldOptions({ collection, compile, getCollectionFields });
|
||||
|
||||
return <Variable.TextArea {...props} scope={options} />;
|
||||
},
|
||||
@ -35,8 +37,10 @@ const InternalExpression = observer(
|
||||
function Result(props) {
|
||||
const { t } = useTranslation();
|
||||
const values = useRecord();
|
||||
const compile = useCompile();
|
||||
const { getCollectionFields } = useCollectionManager();
|
||||
const options = useMemo(
|
||||
() => useCollectionFieldOptions({ collection: values.sourceCollection }),
|
||||
() => getCollectionFieldOptions({ collection: values.sourceCollection, compile, getCollectionFields }),
|
||||
[values.sourceCollection, values.sourceCollection],
|
||||
);
|
||||
return props.value ? (
|
||||
|
@ -18,8 +18,12 @@ import { FieldsSelect } from '../components/FieldsSelect';
|
||||
import { ValueBlock } from '../components/ValueBlock';
|
||||
import { useNodeContext } from '.';
|
||||
|
||||
function matchToManyField(field, depth): boolean {
|
||||
return ['hasMany', 'belongsToMany'].includes(field.type) && depth;
|
||||
function matchToManyField(field, appends): boolean {
|
||||
const fieldPrefix = `${field.name}.`;
|
||||
return (
|
||||
['hasMany', 'belongsToMany'].includes(field.type) &&
|
||||
(appends.includes(field.name) || appends.some((item) => item.startsWith(fieldPrefix)))
|
||||
);
|
||||
}
|
||||
|
||||
function AssociatedConfig({ value, onChange, ...props }): JSX.Element {
|
||||
|
@ -14,18 +14,20 @@ import { BaseTypeSets, useWorkflowVariableOptions } from '../variable';
|
||||
import { ValueBlock } from '../components/ValueBlock';
|
||||
|
||||
function useDynamicExpressionCollectionFieldMatcher(field): boolean {
|
||||
const { getCollectionFields } = useCollectionManager();
|
||||
if (field.type !== 'belongsTo') {
|
||||
if (!['belongsTo', 'hasOne'].includes(field.type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const fields = getCollectionFields(field.target);
|
||||
const fields = this.getCollectionFields(field.target);
|
||||
return fields.some((f) => f.interface === 'expression');
|
||||
}
|
||||
|
||||
const DynamicConfig = ({ value, onChange }) => {
|
||||
const { t } = useTranslation();
|
||||
const scope = useWorkflowVariableOptions({ types: [useDynamicExpressionCollectionFieldMatcher] });
|
||||
const { getCollectionFields } = useCollectionManager();
|
||||
const scope = useWorkflowVariableOptions({
|
||||
types: [useDynamicExpressionCollectionFieldMatcher.bind({ getCollectionFields })],
|
||||
});
|
||||
|
||||
return (
|
||||
<FormLayout layout="vertical">
|
||||
@ -55,10 +57,6 @@ const DynamicConfig = ({ value, onChange }) => {
|
||||
);
|
||||
};
|
||||
|
||||
function useWorkflowVariableEntityOptions() {
|
||||
return useWorkflowVariableOptions({ types: [{ type: 'reference', options: { collection: '*', entity: true } }] });
|
||||
}
|
||||
|
||||
export default {
|
||||
title: `{{t("Calculation", { ns: "${NAMESPACE}" })}}`,
|
||||
type: 'calculation',
|
||||
@ -94,10 +92,13 @@ export default {
|
||||
type: 'string',
|
||||
title: `{{t("Calculation expression", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Variable.TextArea',
|
||||
'x-component-props': {
|
||||
scope: '{{useWorkflowVariableOptions}}',
|
||||
},
|
||||
'x-component': 'CalculationExpression',
|
||||
// NOTE: can not use Variable.Input and scope directly as below,
|
||||
// because the scope will be cached.
|
||||
// 'x-component': 'Variable.Input',
|
||||
// 'x-component-props': {
|
||||
// scope: '{{useWorkflowVariableOptions()}}',
|
||||
// },
|
||||
['x-validator'](value, rules, { form }) {
|
||||
const { values } = form;
|
||||
const { evaluate } = evaluators.get(values.engine) as Evaluator;
|
||||
@ -133,15 +134,15 @@ export default {
|
||||
type: 'string',
|
||||
title: `{{t("Variable datasource", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Variable.Input',
|
||||
'x-component': 'ScopeSelect',
|
||||
'x-component-props': {
|
||||
scope: '{{useWorkflowVariableEntityOptions}}',
|
||||
changeOnSelect: true,
|
||||
},
|
||||
'x-reactions': {
|
||||
dependencies: ['dynamic'],
|
||||
fulfill: {
|
||||
state: {
|
||||
visible: '{{$deps[0] !== false}}',
|
||||
visible: '{{$deps[0]}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -149,11 +150,20 @@ export default {
|
||||
},
|
||||
view: {},
|
||||
scope: {
|
||||
useWorkflowVariableOptions,
|
||||
useWorkflowVariableEntityOptions,
|
||||
renderEngineReference,
|
||||
},
|
||||
components: {
|
||||
CalculationExpression(props) {
|
||||
const scope = useWorkflowVariableOptions();
|
||||
|
||||
return <Variable.TextArea scope={scope} {...props} />;
|
||||
},
|
||||
ScopeSelect(props) {
|
||||
const scope = useWorkflowVariableOptions({
|
||||
types: [{ type: 'reference', options: { collection: '*', entity: true } }],
|
||||
});
|
||||
return <Variable.Input scope={scope} {...props} />;
|
||||
},
|
||||
CalculationResult({ dataSource }) {
|
||||
const { execution } = useFlowContext();
|
||||
if (!execution) {
|
||||
|
@ -6,6 +6,7 @@ import { Registry } from '@nocobase/utils/client';
|
||||
import { Button, Select } from 'antd';
|
||||
import React from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { NodeDefaultView } from '.';
|
||||
import { Branch } from '../Branch';
|
||||
import { useFlowContext } from '../FlowContext';
|
||||
@ -371,10 +372,7 @@ export default {
|
||||
type: 'string',
|
||||
title: `{{t("Condition expression", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Variable.TextArea',
|
||||
'x-component-props': {
|
||||
scope: '{{useWorkflowVariableOptions}}',
|
||||
},
|
||||
'x-component': 'CalculationExpression',
|
||||
['x-validator'](value, rules, { form }) {
|
||||
const { values } = form;
|
||||
const { evaluate } = evaluators.get(values.engine);
|
||||
@ -483,6 +481,11 @@ export default {
|
||||
},
|
||||
components: {
|
||||
CalculationConfig,
|
||||
CalculationExpression(props) {
|
||||
const scope = useWorkflowVariableOptions();
|
||||
|
||||
return <Variable.TextArea scope={scope} {...props} />;
|
||||
},
|
||||
RadioWithTooltip,
|
||||
},
|
||||
};
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { SchemaInitializerItemOptions, useCollectionDataSource } from '@nocobase/client';
|
||||
import { SchemaInitializerItemOptions, useCollectionDataSource, useCollectionManager, useCompile } from '@nocobase/client';
|
||||
|
||||
import { appends, collection, values } from '../schemas/collection';
|
||||
import CollectionFieldset from '../components/CollectionFieldset';
|
||||
import { NAMESPACE } from '../locale';
|
||||
import { CollectionBlockInitializer } from '../components/CollectionBlockInitializer';
|
||||
import { useCollectionFieldOptions } from '../variable';
|
||||
import { getCollectionFieldOptions } from '../variable';
|
||||
import { FieldsSelect } from '../components/FieldsSelect';
|
||||
|
||||
export default {
|
||||
@ -41,10 +41,18 @@ export default {
|
||||
FieldsSelect,
|
||||
},
|
||||
useVariables({ config }, options) {
|
||||
const result = useCollectionFieldOptions({
|
||||
collection: config?.collection,
|
||||
const compile = useCompile();
|
||||
const { getCollectionFields } = useCollectionManager();
|
||||
// const depth = config?.params?.appends?.length
|
||||
// ? config?.params?.appends.reduce((max, item) => Math.max(max, item.split('.').length), 1)
|
||||
// : 0;
|
||||
const result = getCollectionFieldOptions({
|
||||
collection: config.collection,
|
||||
...options,
|
||||
depth: options?.depth ?? config?.params?.appends?.length ? 1 : 0,
|
||||
// depth: options?.depth ?? depth,
|
||||
appends: config.params?.appends,
|
||||
compile,
|
||||
getCollectionFields,
|
||||
});
|
||||
|
||||
return result?.length ? result : null;
|
||||
|
@ -40,8 +40,20 @@ export default {
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Variable.Input',
|
||||
'x-component-props': {
|
||||
scope: '{{useWorkflowVariableOptions}}',
|
||||
scope: '{{useWorkflowVariableOptions()}}',
|
||||
changeOnSelect: true,
|
||||
useTypedConstant: ['string', 'number', 'null'],
|
||||
className: css`
|
||||
width: 100%;
|
||||
|
||||
.variable {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ant-input.null-value {
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { BlockInitializers, SchemaInitializerItemOptions, useCollectionManager } from '@nocobase/client';
|
||||
import { BlockInitializers, SchemaInitializerItemOptions, useCollectionManager, useCompile } from '@nocobase/client';
|
||||
|
||||
import { CollectionBlockInitializer } from '../../components/CollectionBlockInitializer';
|
||||
import { useCollectionFieldOptions } from '../../variable';
|
||||
import { getCollectionFieldOptions } from '../../variable';
|
||||
import { NAMESPACE } from '../../locale';
|
||||
import { SchemaConfig, SchemaConfigButton } from './SchemaConfig';
|
||||
import { ModeConfig } from './ModeConfig';
|
||||
@ -86,6 +86,8 @@ export default {
|
||||
AssigneesSelect,
|
||||
},
|
||||
useVariables({ config }, { types }) {
|
||||
const compile = useCompile();
|
||||
const { getCollectionFields } = useCollectionManager();
|
||||
const formKeys = Object.keys(config.forms ?? {});
|
||||
if (!formKeys.length) {
|
||||
return null;
|
||||
@ -96,10 +98,12 @@ export default {
|
||||
const form = config.forms[formKey];
|
||||
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const options = useCollectionFieldOptions({
|
||||
const options = getCollectionFieldOptions({
|
||||
fields: form.collection?.fields,
|
||||
collection: form.collection,
|
||||
types,
|
||||
compile,
|
||||
getCollectionFields,
|
||||
});
|
||||
return options.length
|
||||
? {
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { SchemaInitializerItemOptions, useCollectionDataSource } from '@nocobase/client';
|
||||
import { SchemaInitializerItemOptions, useCollectionDataSource, useCollectionManager, useCompile } from '@nocobase/client';
|
||||
|
||||
import { appends, collection, filter } from '../schemas/collection';
|
||||
import { NAMESPACE } from '../locale';
|
||||
import { CollectionBlockInitializer } from '../components/CollectionBlockInitializer';
|
||||
import { FilterDynamicComponent } from '../components/FilterDynamicComponent';
|
||||
import { useCollectionFieldOptions } from '../variable';
|
||||
import { getCollectionFieldOptions } from '../variable';
|
||||
import { FieldsSelect } from '../components/FieldsSelect';
|
||||
|
||||
export default {
|
||||
@ -44,10 +44,18 @@ export default {
|
||||
FieldsSelect,
|
||||
},
|
||||
useVariables({ config }, options) {
|
||||
const result = useCollectionFieldOptions({
|
||||
collection: config?.collection,
|
||||
const compile = useCompile();
|
||||
const { getCollectionFields } = useCollectionManager();
|
||||
// const depth = config?.params?.appends?.length
|
||||
// ? config?.params?.appends.reduce((max, item) => Math.max(max, item.split('.').length), 1)
|
||||
// : 0;
|
||||
const result = getCollectionFieldOptions({
|
||||
collection: config.collection,
|
||||
...options,
|
||||
depth: options?.depth ?? config?.params?.appends?.length ? 1 : 0,
|
||||
// depth: options?.depth ?? depth,
|
||||
appends: config.params?.appends,
|
||||
compile,
|
||||
getCollectionFields,
|
||||
});
|
||||
|
||||
return result?.length ? result : null;
|
||||
|
@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
import { ArrayItems } from '@formily/antd';
|
||||
|
||||
import { Variable } from '@nocobase/client';
|
||||
import { NAMESPACE } from '../locale';
|
||||
import { useWorkflowVariableOptions } from '../variable';
|
||||
|
||||
@ -66,7 +68,7 @@ export default {
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Variable.Input',
|
||||
'x-component-props': {
|
||||
scope: useWorkflowVariableOptions,
|
||||
scope: '{{useWorkflowVariableOptions()}}',
|
||||
useTypedConstant: true,
|
||||
},
|
||||
},
|
||||
@ -112,7 +114,7 @@ export default {
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Variable.Input',
|
||||
'x-component-props': {
|
||||
scope: useWorkflowVariableOptions,
|
||||
scope: '{{useWorkflowVariableOptions()}}',
|
||||
useTypedConstant: true,
|
||||
},
|
||||
},
|
||||
@ -138,9 +140,9 @@ export default {
|
||||
title: `{{t("Body", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-decorator-props': {},
|
||||
'x-component': 'Variable.JSON',
|
||||
'x-component': 'RequestBody',
|
||||
'x-component-props': {
|
||||
scope: useWorkflowVariableOptions,
|
||||
changeOnSelect: true,
|
||||
autoSize: {
|
||||
minRows: 10,
|
||||
},
|
||||
@ -170,8 +172,14 @@ export default {
|
||||
},
|
||||
},
|
||||
view: {},
|
||||
scope: {},
|
||||
scope: {
|
||||
useWorkflowVariableOptions,
|
||||
},
|
||||
components: {
|
||||
ArrayItems,
|
||||
RequestBody(props) {
|
||||
const scope = useWorkflowVariableOptions();
|
||||
return <Variable.JSON scope={scope} {...props} />;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { SchemaInitializerItemOptions, useCollectionDataSource } from '@nocobase/client';
|
||||
import { SchemaInitializerItemOptions, useCollectionDataSource, useCollectionManager, useCompile } from '@nocobase/client';
|
||||
import { CollectionBlockInitializer } from '../components/CollectionBlockInitializer';
|
||||
import { FieldsSelect } from '../components/FieldsSelect';
|
||||
import { NAMESPACE, useWorkflowTranslation } from '../locale';
|
||||
import { NAMESPACE, lang } from '../locale';
|
||||
import { appends, collection, filter } from '../schemas/collection';
|
||||
import { useCollectionFieldOptions } from '../variable';
|
||||
import { getCollectionFieldOptions } from '../variable';
|
||||
|
||||
const COLLECTION_TRIGGER_MODE = {
|
||||
CREATED: 1,
|
||||
@ -45,6 +45,15 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
target: 'appends',
|
||||
effects: ['onFieldValueChange'],
|
||||
fulfill: {
|
||||
state: {
|
||||
value: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
mode: {
|
||||
@ -133,7 +142,8 @@ export default {
|
||||
FieldsSelect,
|
||||
},
|
||||
useVariables(config, options) {
|
||||
const { t } = useWorkflowTranslation();
|
||||
const compile = useCompile();
|
||||
const { getCollectionFields } = useCollectionManager();
|
||||
const rootFields = [
|
||||
{
|
||||
collectionName: config.collection,
|
||||
@ -141,14 +151,20 @@ export default {
|
||||
type: 'hasOne',
|
||||
target: config.collection,
|
||||
uiSchema: {
|
||||
title: t('Trigger data'),
|
||||
title: lang('Trigger data'),
|
||||
},
|
||||
},
|
||||
];
|
||||
const result = useCollectionFieldOptions({
|
||||
// const depth = config.appends?.length
|
||||
// ? config.appends.reduce((max, item) => Math.max(max, item.split('.').length), 1) + 1
|
||||
// : 1;
|
||||
const result = getCollectionFieldOptions({
|
||||
// depth,
|
||||
...options,
|
||||
fields: rootFields,
|
||||
depth: options?.depth ?? (config.appends?.length ? 2 : 1),
|
||||
appends: ['data', ...(config.appends?.map((item) => `data.${item}`) || [])],
|
||||
compile,
|
||||
getCollectionFields,
|
||||
});
|
||||
return result;
|
||||
},
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { useCollectionDataSource, SchemaInitializerItemOptions } from '@nocobase/client';
|
||||
import { useCollectionDataSource, SchemaInitializerItemOptions, useCompile, useCollectionManager } from '@nocobase/client';
|
||||
|
||||
import { ScheduleConfig } from './ScheduleConfig';
|
||||
import { SCHEDULE_MODE } from './constants';
|
||||
import { NAMESPACE, useWorkflowTranslation } from '../../locale';
|
||||
import { NAMESPACE, lang } from '../../locale';
|
||||
import { CollectionBlockInitializer } from '../../components/CollectionBlockInitializer';
|
||||
import { useCollectionFieldOptions } from '../../variable';
|
||||
import { getCollectionFieldOptions } from '../../variable';
|
||||
import { FieldsSelect } from '../../components/FieldsSelect';
|
||||
|
||||
export default {
|
||||
@ -24,20 +24,31 @@ export default {
|
||||
ScheduleConfig,
|
||||
FieldsSelect,
|
||||
},
|
||||
useVariables(config, { types }) {
|
||||
const { t } = useWorkflowTranslation();
|
||||
useVariables(config, opts) {
|
||||
const compile = useCompile();
|
||||
const { getCollectionFields } = useCollectionManager();
|
||||
const options: any[] = [];
|
||||
if (!types || types.includes('date')) {
|
||||
options.push({ key: 'date', value: 'date', label: t('Trigger time') });
|
||||
if (!opts?.types || opts.types.includes('date')) {
|
||||
options.push({ key: 'date', value: 'date', label: lang('Trigger time') });
|
||||
}
|
||||
|
||||
const fieldOptions = useCollectionFieldOptions({ collection: config.collection });
|
||||
const depth = config.appends?.length
|
||||
? config.appends.reduce((max, item) => Math.max(max, item.split('.').length), 1) + 1
|
||||
: 1;
|
||||
|
||||
const fieldOptions = getCollectionFieldOptions({
|
||||
depth,
|
||||
...opts,
|
||||
collection: config.collection,
|
||||
compile,
|
||||
getCollectionFields,
|
||||
});
|
||||
if (config.mode === SCHEDULE_MODE.COLLECTION_FIELD) {
|
||||
if (fieldOptions.length) {
|
||||
options.push({
|
||||
key: 'data',
|
||||
value: 'data',
|
||||
label: t('Trigger data'),
|
||||
label: lang('Trigger data'),
|
||||
children: fieldOptions,
|
||||
});
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useCollectionManager, useCompile } from '@nocobase/client';
|
||||
import { useFlowContext } from './FlowContext';
|
||||
import { NAMESPACE } from './locale';
|
||||
import { NAMESPACE, lang } from './locale';
|
||||
import { instructions, useAvailableUpstreams, useNodeContext, useUpstreamScopes } from './nodes';
|
||||
import { triggers } from './triggers';
|
||||
|
||||
@ -79,7 +79,7 @@ export const systemOptions = {
|
||||
{
|
||||
key: 'now',
|
||||
value: 'now',
|
||||
label: `{{t("System time")}}`,
|
||||
label: lang('System time'),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
@ -110,7 +110,7 @@ export const BaseTypeSets = {
|
||||
// { type: 'reference', options: { collection: 'attachments', multiple: false } }
|
||||
// { type: 'reference', options: { collection: 'myExpressions', entity: false } }
|
||||
|
||||
function matchFieldType(field, type, depth): boolean {
|
||||
function matchFieldType(field, type, appends): boolean {
|
||||
const inputType = typeof type;
|
||||
if (inputType === 'string') {
|
||||
return BaseTypeSets[type]?.has(field.interface);
|
||||
@ -132,7 +132,7 @@ function matchFieldType(field, type, depth): boolean {
|
||||
}
|
||||
|
||||
if (inputType === 'function') {
|
||||
return type(field, depth);
|
||||
return type(field, appends);
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -142,31 +142,47 @@ function isAssociationField(field): boolean {
|
||||
return ['belongsTo', 'hasOne', 'hasMany', 'belongsToMany'].includes(field.type);
|
||||
}
|
||||
|
||||
export function filterTypedFields(fields, types, depth = 1) {
|
||||
if (!types) {
|
||||
return fields;
|
||||
function getNextAppends(field, appends: string[]) {
|
||||
const fieldPrefix = `${field.name}.`;
|
||||
return appends.filter((item) => item.startsWith(fieldPrefix)).map((item) => item.replace(fieldPrefix, ''));
|
||||
}
|
||||
|
||||
function filterTypedFields({ fields, types, appends, compile, getCollectionFields }) {
|
||||
return fields.filter((field) => {
|
||||
if (
|
||||
isAssociationField(field) &&
|
||||
depth &&
|
||||
filterTypedFields(useNormalizedFields(field.target), types, depth - 1).length
|
||||
) {
|
||||
return true;
|
||||
const match = types?.length ? types.some((type) => matchFieldType(field, type, appends)) : true;
|
||||
if (isAssociationField(field)) {
|
||||
const nextAppends = getNextAppends(field, appends);
|
||||
const included = appends.includes(field.name);
|
||||
if (match) {
|
||||
return included;
|
||||
} else {
|
||||
return (
|
||||
(nextAppends.length || included) &&
|
||||
filterTypedFields({
|
||||
fields: getNormalizedFields(field.target, { compile, getCollectionFields }),
|
||||
types,
|
||||
// depth: depth - 1,
|
||||
appends: nextAppends,
|
||||
compile,
|
||||
getCollectionFields,
|
||||
}).length
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return match;
|
||||
}
|
||||
return types.some((type) => matchFieldType(field, type, depth));
|
||||
});
|
||||
}
|
||||
|
||||
export function useWorkflowVariableOptions(options = {}) {
|
||||
const compile = useCompile();
|
||||
const result = [scopeOptions, nodesOptions, triggerOptions, systemOptions].map((item: any) => {
|
||||
const opts = typeof item.useOptions === 'function' ? item.useOptions(options).filter(Boolean) : null;
|
||||
const opts = item.useOptions(options).filter(Boolean);
|
||||
return {
|
||||
label: compile(item.label),
|
||||
value: item.value,
|
||||
key: item.value,
|
||||
children: compile(opts),
|
||||
children: opts,
|
||||
disabled: opts && !opts.length,
|
||||
};
|
||||
});
|
||||
@ -174,9 +190,7 @@ export function useWorkflowVariableOptions(options = {}) {
|
||||
return result;
|
||||
}
|
||||
|
||||
function useNormalizedFields(collectionName) {
|
||||
const compile = useCompile();
|
||||
const { getCollectionFields } = useCollectionManager();
|
||||
function getNormalizedFields(collectionName, { compile, getCollectionFields }) {
|
||||
const fields = getCollectionFields(collectionName);
|
||||
const foreignKeyFields: any[] = [];
|
||||
const otherFields: any[] = [];
|
||||
@ -231,24 +245,55 @@ function useNormalizedFields(collectionName) {
|
||||
return otherFields.filter((field) => field.interface && !field.hidden);
|
||||
}
|
||||
|
||||
export function useCollectionFieldOptions(options): VariableOption[] {
|
||||
const { fields, collection, types, depth = 1 } = options;
|
||||
const compile = useCompile();
|
||||
const normalizedFields = useNormalizedFields(collection);
|
||||
async function loadChildren(option) {
|
||||
const result = getCollectionFieldOptions({
|
||||
collection: option.field.target,
|
||||
types: option.types,
|
||||
appends: getNextAppends(option.field, option.appends),
|
||||
sourceKey: option.field.key,
|
||||
compile: this.compile,
|
||||
getCollectionFields: this.getCollectionFields,
|
||||
});
|
||||
if (result.length) {
|
||||
option.children = result;
|
||||
} else {
|
||||
option.isLeaf = true;
|
||||
option.loadChildren = null;
|
||||
const matchingType = option.types?.some((type) => matchFieldType(option.field, type, 0));
|
||||
if (!matchingType) {
|
||||
option.disabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getCollectionFieldOptions(options): VariableOption[] {
|
||||
const { fields, collection, types, appends = [], compile, getCollectionFields } = options;
|
||||
const normalizedFields = getNormalizedFields(collection, { compile, getCollectionFields });
|
||||
const computedFields = fields ?? normalizedFields;
|
||||
const result: VariableOption[] = filterTypedFields(computedFields, types, depth)
|
||||
.filter((field) => !isAssociationField(field) || depth)
|
||||
.map((field) => {
|
||||
const boundLoadChildren = loadChildren.bind({ compile, getCollectionFields });
|
||||
|
||||
const result: VariableOption[] = filterTypedFields({
|
||||
fields: computedFields,
|
||||
types,
|
||||
// depth,
|
||||
appends,
|
||||
compile,
|
||||
getCollectionFields,
|
||||
}).map((field) => {
|
||||
const label = compile(field.uiSchema?.title || field.name);
|
||||
const nextAppends = getNextAppends(field, appends);
|
||||
// TODO: no matching fields in next appends should consider isLeaf as true
|
||||
const isLeaf = !isAssociationField(field) || (!nextAppends.length && !appends.includes(field.name));
|
||||
return {
|
||||
label,
|
||||
key: field.name,
|
||||
value: field.name,
|
||||
children:
|
||||
isAssociationField(field) && depth
|
||||
? useCollectionFieldOptions({ collection: field.target, types, depth: depth - 1 })
|
||||
: null,
|
||||
isLeaf,
|
||||
loadChildren: isLeaf ? null : boundLoadChildren,
|
||||
field,
|
||||
// depth,
|
||||
appends,
|
||||
types,
|
||||
};
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user